Merge branch 'LOG4J2-2832' of https://github.com/asbachb/logging-log4j2 into asbachb-LOG4J2-2832
diff --git a/.asf.yaml b/.asf.yaml
new file mode 100644
index 0000000..d976e25
--- /dev/null
+++ b/.asf.yaml
@@ -0,0 +1,17 @@
+notifications:
+  commits: commits@logging.apache.org
+  # issues and PRs send their own emails to devs
+  issues: notifications@logging.apache.org
+  pullrequests: notifications@logging.apache.org
+  jira_options: link label
+github:
+  description: "Apache Log4j 2 is an upgrade to Log4j that provides significant improvements over its predecessor, Log4j 1.x, and provides many of the improvements available in Logback while fixing some inherent problems in Logback's architecture."
+  homepage: https://logging.apache.org/log4j/2.x/
+  labels:
+    - apache
+    - api
+    - java
+    - library
+    - log4j
+    - log4j2
+    - logging
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..d0e9890
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,24 @@
+#
+#  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.
+#
+version: 2
+updates:
+- package-ecosystem: maven
+  directory: "/"
+  schedule:
+    interval: daily
+    time: '04:00'
+  open-pull-requests-limit: 10
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..d6338a6
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -0,0 +1,55 @@
+name: Maven
+
+on: [push]
+
+jobs:
+  build:
+
+    runs-on: ${{ matrix.os }}
+
+    strategy:
+      matrix:
+        os: [ubuntu-latest, windows-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 verify --global-toolchains .github/workflows/maven-toolchains.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 verify --global-toolchains ".github\workflows\maven-toolchains.xml"
diff --git a/.gitignore b/.gitignore
index fce0e91..533fdc3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,4 @@
 velocity.log
 felix-cache/
 bin/
+.mvn/wrapper/MavenWrapperDownloader.java
diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
index c6feb8b..2cc7d4a 100644
--- a/.mvn/wrapper/maven-wrapper.jar
+++ b/.mvn/wrapper/maven-wrapper.jar
Binary files differ
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index d183696..a8cc56e 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -13,4 +13,5 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip
\ No newline at end of file
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/BUILDING.md b/BUILDING.md
index d973458..d77b14e 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -15,8 +15,8 @@
  limitations under the License.
 -->
 # Building Log4j 2
-  
-To build Log4j 2, you need a JDK implementation version 1.7 or greater, JDK 
+
+To build Log4j 2, you need a JDK implementation version 1.8 or greater, JDK 
 version 9, and Apache Maven 3.x.
 
 Log4j 2.x uses the Java 9 compiler in addition to 
@@ -35,10 +35,6 @@
 
     mvn apache-rat:check
 
-To build the site with Java 7, make sure you give Maven enough memory using 
-`MAVEN_OPTS` with options appropriate for your JVM. Alternatively, you can 
-build with Java 8 and not deal with `MAVEN_OPTS`. 
-
 To install the jars in your local Maven repository, from a command line, run:
 
     mvn clean install
@@ -51,10 +47,6 @@
 
 Next, to build the site:
 
-If Java 7 runs out of memory building the site, you will need:
-
-    set MAVEN_OPTS=-Xmx2000m -XX:MaxPermSize=384m
-
     mvn site
 
 On Windows, use a local staging directory, for example:
diff --git a/Dockerfile b/Dockerfile
index 07b0b59..540d028 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,27 +13,17 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# FROM openjdk:7-alpine
-# Reverted to debian yet alpine does not include jdk9
-FROM openjdk:7-jdk
-
-# Require while jdk9 is unstable on debian
-RUN echo 'deb http://deb.debian.org/debian unstable main' >> /etc/apt/sources.list
+FROM openjdk:8
 
 RUN set -ex \
-    && mkdir /src \
     && apt-get update \
-    && apt-get install -y \
-       curl \
-       openjdk-9-jdk-headless \
-    && ln -svT "/usr/lib/jvm/java-9-openjdk-$(dpkg --print-architecture)" /docker-java-9-home \
-    && cd /opt \
-    && curl -fsSL http://www-us.apache.org/dist/maven/maven-3/3.5.0/binaries/apache-maven-3.5.0-bin.tar.gz -o maven.tar.gz \
-    && tar -xzf maven.tar.gz \
-    && rm -f maven.tar.gz
+    && apt-get install -y openjdk-11-jdk-headless \
+    && ln -svT "/usr/lib/jvm/java-11-openjdk-$(dpkg --print-architecture)" /usr/local/openjdk-11
+
+VOLUME /src /root/.m2/repository
 
 COPY . /src
 
 RUN set -ex \
     && cd /src \
-    && /opt/apache-maven-3.5.0/bin/mvn verify --global-toolchains toolchains-docker.xml
+    && ./mvnw --toolchains toolchains-docker.xml install
diff --git a/Jenkinsfile b/Jenkinsfile
deleted file mode 100644
index 9a1c0ee..0000000
--- a/Jenkinsfile
+++ /dev/null
@@ -1,161 +0,0 @@
-#!groovy
-/*
- * 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
- *
- *     https://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.
- */
-
-// =================================================================
-// https://cwiki.apache.org/confluence/display/LOGGING/Jenkins+Setup
-// =================================================================
-
-// general pipeline documentation: https://jenkins.io/doc/book/pipeline/syntax/
-pipeline {
-    // https://jenkins.io/doc/book/pipeline/syntax/#options
-    options {
-        // support ANSI colors in stdout/stderr
-        ansiColor 'xterm'
-        // only keep the latest 10 builds
-        buildDiscarder logRotator(numToKeepStr: '10')
-        // cancel build if not complete within two hours of scheduling
-        timeout time: 2, unit: 'HOURS'
-        // fail parallel stages as soon as any of them fail
-        parallelsAlwaysFailFast()
-    }
-    // https://jenkins.io/doc/book/pipeline/syntax/#agent
-    // start with no Jenkins agent allocated as they will only be needed for the individual stages
-    // therefore, anything in the top level post section can only contain steps that don't require a Jenkins agent
-    // (such as slackSend, mail, etc.)
-    agent none
-    stages {
-        stage('Continuous Integration') {
-            // https://jenkins.io/doc/book/pipeline/syntax/#parallel
-            parallel {
-                stage('Ubuntu') {
-                    agent {
-                        // https://cwiki.apache.org/confluence/display/INFRA/Jenkins+node+labels
-                        label 'ubuntu'
-                    }
-                    // https://jenkins.io/doc/book/pipeline/syntax/#tools
-                    tools {
-                        // https://cwiki.apache.org/confluence/display/INFRA/JDK+Installation+Matrix
-                        jdk 'JDK 1.8 (latest)'
-                        // https://cwiki.apache.org/confluence/display/INFRA/Maven+Installation+Matrix
-                        maven 'Maven 3 (latest)'
-                    }
-                    // https://jenkins.io/doc/book/pipeline/syntax/#environment
-                    environment {
-                        LANG = 'C.UTF-8'
-                    }
-                    steps {
-                        // build, test, and deploy snapshots
-                        // note that the jenkins system property is set here to activate certain pom properties in
-                        // some log4j modules that compile against system jars (e.g., log4j-jmx-gui)
-                        // also note that the Jenkins agents on builds.a.o already have an ~/.m2/settings.xml for snapshots
-                        sh 'mvn --show-version --fail-at-end --toolchains toolchains-jenkins-ubuntu.xml -Djenkins clean install deploy'
-                    }
-                    post {
-                        always {
-                            // record linux run of tests
-                            junit '**/*-reports/*.xml'
-                            // additional warnings generated during build
-                            // TODO: would be nice to be able to include checkstyle, cpd, pmd, and spotbugs,
-                            //       but current site build takes too long
-                            recordIssues enabledForFailure: true,
-                                    sourceCodeEncoding: 'UTF-8',
-                                    referenceJobName: 'log4j/master',
-                                    tools: [mavenConsole(), errorProne(), java(),
-                                            taskScanner(highTags: 'FIXME', normalTags: 'TODO', includePattern: '**/*.java', excludePattern: '*/target/**')]
-                        }
-                    }
-                }
-                stage('Windows') {
-                    agent {
-                        // https://cwiki.apache.org/confluence/display/INFRA/Jenkins+node+labels
-                        label 'Windows'
-                    }
-                    tools {
-                        // https://cwiki.apache.org/confluence/display/INFRA/JDK+Installation+Matrix
-                        jdk 'JDK 1.8 (latest)'
-                        // https://cwiki.apache.org/confluence/display/INFRA/Maven+Installation+Matrix
-                        maven 'Maven 3 (latest)'
-                    }
-                    environment {
-                        LANG = 'C.UTF-8'
-                    }
-                    steps {
-                        // note that previous test runs of log4j-mongodb* may have left behind an embedded mongo folder
-                        // also note that we don't need to use the jenkins system property here as it's ubuntu-specific
-                        bat '''
-                    if exist %userprofile%\\.embedmongo\\ rd /s /q %userprofile%\\.embedmongo
-                    mvn --show-version --fail-at-end --toolchains toolchains-jenkins-win.xml clean install
-                    '''
-                    }
-                    post {
-                        always {
-                            // record windows run of tests
-                            junit '**/*-reports/*.xml'
-                        }
-                    }
-                }
-            }
-        }
-    }
-    post {
-        fixed {
-            slackSend channel: 'logging',
-                    color: 'good',
-                    message: ":excellent: <${env.JOB_URL}|${env.JOB_NAME}> was fixed in <${env.BUILD_URL}|build #${env.BUILD_NUMBER}>."
-            mail to: 'notifications@logging.apache.org',
-                    from: 'Mr. Jenkins <jenkins@builds.apache.org>',
-                    subject: "[CI][SUCCESS] ${env.JOB_NAME}#${env.BUILD_NUMBER} back to normal",
-                    body: """
-The build for ${env.JOB_NAME} completed successfully and is back to normal.
-
-Build: ${env.BUILD_URL}
-Logs: ${env.BUILD_URL}console
-Changes: ${env.BUILD_URL}changes
-
---
-Mr. Jenkins
-Director of Continuous Integration
-"""
-        }
-        failure {
-            slackSend channel: 'logging',
-                    color: 'danger',
-                    message: ":doh: <${env.JOB_URL}|${env.JOB_NAME}> failed in <${env.BUILD_URL}|build #${env.BUILD_NUMBER}>. <${env.BUILD_URL}testReport/|Tests>."
-            mail to: 'notifications@logging.apache.org',
-                    from: 'Mr. Jenkins <jenkins@builds.apache.org>',
-                    subject: "[CI][FAILURE] ${env.JOB_NAME}#${env.BUILD_NUMBER} has potential issues",
-                    body: """
-There is a build failure in ${env.JOB_NAME}.
-
-Build: ${env.BUILD_URL}
-Logs: ${env.BUILD_URL}console
-Test results: ${env.BUILD_URL}testReport/
-Changes: ${env.BUILD_URL}changes
-
---
-Mr. Jenkins
-Director of Continuous Integration
-"""
-        }
-        unstable {
-            slackSend channel: 'logging',
-                    color: 'warning',
-                    message: ":disappear: <${env.JOB_URL}|${env.JOB_NAME}> is unstable in <${env.BUILD_URL}|build #${env.BUILD_NUMBER}>."
-        }
-    }
-}
diff --git a/README.md b/README.md
index 85f5276..79ae4f2 100644
--- a/README.md
+++ b/README.md
@@ -3,10 +3,10 @@
 Apache Log4j 2 is an upgrade to Log4j that provides significant improvements over its predecessor, Log4j 1.x,
 and provides many of the improvements available in Logback while fixing some inherent problems in Logback's architecture.
 
-[![Jenkins Status (master branch)](https://builds.apache.org/buildStatus/icon?job=log4j%2Fmaster&subject=master&style=flat)](https://builds.apache.org/job/log4j/job/master/)
-[![Jenkins Status (release-2.x branch)](https://builds.apache.org/buildStatus/icon?job=log4j%2Frelease-2.x&subject=release-2.x&style=flat)](https://builds.apache.org/job/log4j/job/release-2.x/)
-[![Travis Status](https://travis-ci.org/apache/logging-log4j2.svg?branch=master)](https://travis-ci.org/apache/logging-log4j2)
-[![Maven Central](https://img.shields.io/maven-central/v/org.apache.logging.log4j/log4j-api.svg)](http://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api)
+[![Jenkins build (3.x)](https://img.shields.io/jenkins/build?jobUrl=https%3A%2F%2Fci-builds.apache.org%2Fjob%2FLogging%2Fjob%2Flog4j%2Fjob%2Fmaster%2F&label=3.x&logo=cloudbees)](https://ci-builds.apache.org/job/Logging/job/log4j/job/master/)
+[![Jenkins build (2.x)](https://img.shields.io/jenkins/build?jobUrl=https%3A%2F%2Fci-builds.apache.org%2Fjob%2FLogging%2Fjob%2Flog4j%2Fjob%2Frelease-2.x%2F&label=2.x&logo=cloudbees)](https://ci-builds.apache.org/job/Logging/job/log4j/job/release-2.x/)
+[![GitHub build (3.x)](https://github.com/apache/logging-log4j2/workflows/Maven/badge.svg)](https://github.com/apache/logging-log4j2/actions?query=workflow%3AMaven+branch%3Amaster)
+[![Maven Central](https://img.shields.io/maven-central/v/org.apache.logging.log4j/log4j-api.svg?logo=java)](http://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api)
 
 
 ## Pull Requests on Github
@@ -14,7 +14,7 @@
 By sending a pull request you grant the Apache Software Foundation sufficient rights to use and release the submitted 
 work under the Apache license. You grant the same rights (copyright license, patent license, etc.) to the 
 Apache Software Foundation as if you have signed a Contributor License Agreement. For contributions that are 
-judged to be non-trivial, you will be asked to actually signing a Contributor License Agreement.
+judged to be non-trivial, you will be asked to actually sign a [Contributor License Agreement](https://www.apache.org/licenses/icla.pdf).
 
 ## Usage
 
diff --git a/log4j-1.2-api/src/main/java/org/apache/log4j/bridge/LogEventWrapper.java b/log4j-1.2-api/src/main/java/org/apache/log4j/bridge/LogEventWrapper.java
index 1e46e12..4ab9db9 100644
--- a/log4j-1.2-api/src/main/java/org/apache/log4j/bridge/LogEventWrapper.java
+++ b/log4j-1.2-api/src/main/java/org/apache/log4j/bridge/LogEventWrapper.java
@@ -20,6 +20,7 @@
 import org.apache.log4j.helpers.OptionConverter;
 import org.apache.log4j.spi.LocationInfo;
 import org.apache.log4j.spi.LoggingEvent;
+import org.apache.log4j.spi.ThrowableInformation;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.ThreadContext;
@@ -36,6 +37,7 @@
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * Exposes a Log4j 1 logging event as a Log4j 2 LogEvent.
@@ -45,17 +47,19 @@
     private final LoggingEvent event;
     private final ContextDataMap contextData;
     private final MutableThreadContextStack contextStack;
-    private volatile Thread thread;
+    private Thread thread;
 
     public LogEventWrapper(LoggingEvent event) {
         this.event = event;
         this.contextData = new ContextDataMap(event.getProperties());
         this.contextStack = new MutableThreadContextStack(NDC.cloneStack());
+        this.thread = Objects.equals(event.getThreadName(), Thread.currentThread().getName())
+                ? Thread.currentThread() : null;
     }
 
     @Override
     public LogEvent toImmutable() {
-        return null;
+        return this;
     }
 
     @Override
@@ -125,12 +129,12 @@
 
     @Override
     public int getThreadPriority() {
-            Thread thread = getThread();
-            return thread != null ? thread.getPriority() : 0;
+        Thread thread = getThread();
+        return thread != null ? thread.getPriority() : 0;
     }
 
     private Thread getThread() {
-        if (thread == null) {
+        if (thread == null && event.getThreadName() != null) {
             for (Thread thread : Thread.getAllStackTraces().keySet()) {
                 if (thread.getName().equals(event.getThreadName())) {
                     this.thread = thread;
@@ -138,15 +142,13 @@
                 }
             }
         }
-        return null;
+        return thread;
     }
 
     @Override
     public Throwable getThrown() {
-        if (event.getThrowableInformation() != null) {
-            return event.getThrowableInformation().getThrowable();
-        }
-        return null;
+        ThrowableInformation throwableInformation = event.getThrowableInformation();
+        return throwableInformation == null ? null : throwableInformation.getThrowable();
     }
 
     @Override
diff --git a/log4j-1.2-api/src/main/java/org/apache/log4j/config/PropertiesConfiguration.java b/log4j-1.2-api/src/main/java/org/apache/log4j/config/PropertiesConfiguration.java
index 272367c..2ceffaf 100644
--- a/log4j-1.2-api/src/main/java/org/apache/log4j/config/PropertiesConfiguration.java
+++ b/log4j-1.2-api/src/main/java/org/apache/log4j/config/PropertiesConfiguration.java
@@ -390,7 +390,7 @@
                         logger.getName());
                 logger.addAppender(getAppender(appenderName), null, null);
             } else {
-                LOGGER.debug("Appender named [{}}] not found.", appenderName);
+                LOGGER.debug("Appender named [{}] not found.", appenderName);
             }
         }
     }
diff --git a/log4j-1.2-api/src/main/java/org/apache/log4j/xml/XmlConfiguration.java b/log4j-1.2-api/src/main/java/org/apache/log4j/xml/XmlConfiguration.java
index d532a13..8e256bc 100644
--- a/log4j-1.2-api/src/main/java/org/apache/log4j/xml/XmlConfiguration.java
+++ b/log4j-1.2-api/src/main/java/org/apache/log4j/xml/XmlConfiguration.java
@@ -386,7 +386,7 @@
                                     appender.getName());
                             aa.addAppender(child);
                         } else {
-                            LOGGER.error("Requesting attachment of appender named [{}] to appender named [{}}]"
+                            LOGGER.error("Requesting attachment of appender named [{}] to appender named [{}]"
                                             + "which does not implement org.apache.log4j.spi.AppenderAttachable.",
                                     refName, appender.getName());
                         }
@@ -545,7 +545,7 @@
                                 loggerConfig.getName());
                         loggerConfig.addAppender(getAppender(refName), null, null);
                     } else {
-                        LOGGER.debug("Appender named [{}}] not found.", refName);
+                        LOGGER.debug("Appender named [{}] not found.", refName);
                     }
                     break;
                 }
@@ -624,7 +624,7 @@
         }
 
         String priStr = subst(element.getAttribute(VALUE_ATTR));
-        LOGGER.debug("Level value for {} is [{}}].", catName, priStr);
+        LOGGER.debug("Level value for {} is [{}].", catName, priStr);
 
         if (INHERITED.equalsIgnoreCase(priStr) || NULL.equalsIgnoreCase(priStr)) {
             if (isRoot) {
diff --git a/log4j-1.2-api/src/src/main/java/org/apache/log4j/xml/XmlConfigurationFactory.java b/log4j-1.2-api/src/src/main/java/org/apache/log4j/xml/XmlConfigurationFactory.java
index a2f7dc7..0dc66ee 100644
--- a/log4j-1.2-api/src/src/main/java/org/apache/log4j/xml/XmlConfigurationFactory.java
+++ b/log4j-1.2-api/src/src/main/java/org/apache/log4j/xml/XmlConfigurationFactory.java
@@ -472,7 +472,7 @@
                                         appender.getName());
                                 aa.addAppender(child);
                             } else {
-                                LOGGER.error("Requesting attachment of appender named [{}] to appender named [{}}]"
+                                LOGGER.error("Requesting attachment of appender named [{}] to appender named [{}]"
                                                 + "which does not implement org.apache.log4j.spi.AppenderAttachable.",
                                         refName, appender.getName());
                             }
@@ -590,7 +590,7 @@
                                 loggerConfig.getName());
                         loggerConfig.addAppender(configuration.getAppender(refName), null, null);
                     } else {
-                        LOGGER.debug("Appender named [{}}] not found.", refName);
+                        LOGGER.debug("Appender named [{}] not found.", refName);
                     }
                     break;
                 }
@@ -667,7 +667,7 @@
         }
 
         String priStr = subst(element.getAttribute(VALUE_ATTR));
-        LOGGER.debug("Level value for {} is [{}}].", catName, priStr);
+        LOGGER.debug("Level value for {} is [{}].", catName, priStr);
 
         if (INHERITED.equalsIgnoreCase(priStr) || NULL.equalsIgnoreCase(priStr)) {
             if (isRoot) {
diff --git a/log4j-1.2-api/src/test/java/org/apache/log4j/bridge/LogEventWrapperTest.java b/log4j-1.2-api/src/test/java/org/apache/log4j/bridge/LogEventWrapperTest.java
new file mode 100644
index 0000000..af38045
--- /dev/null
+++ b/log4j-1.2-api/src/test/java/org/apache/log4j/bridge/LogEventWrapperTest.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.log4j.bridge;
+
+import org.apache.log4j.spi.LoggingEvent;
+import org.apache.logging.log4j.core.LogEvent;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+public class LogEventWrapperTest {
+
+    @Test
+    public void testThread() {
+        Thread currentThread = Thread.currentThread();
+        String threadName = currentThread.getName();
+        LoggingEvent log4j1Event = new LoggingEvent() {
+
+            @Override
+            public String getThreadName() {
+                return threadName;
+            }
+        };
+        LogEvent log4j2Event = new LogEventWrapper(log4j1Event);
+        assertEquals(currentThread.getId(), log4j2Event.getThreadId());
+        assertEquals(currentThread.getPriority(), log4j2Event.getThreadPriority());
+    }
+
+    @Test
+    public void testToImmutable() {
+        LogEventWrapper wrapper = new LogEventWrapper(new LoggingEvent());
+        assertSame(wrapper, wrapper.toImmutable());
+    }
+}
diff --git a/log4j-1.2-api/src/test/java/org/apache/log4j/config/Log4j1ConfigurationFactoryTest.java b/log4j-1.2-api/src/test/java/org/apache/log4j/config/Log4j1ConfigurationFactoryTest.java
index ebe3e54..7d40a3c 100644
--- a/log4j-1.2-api/src/test/java/org/apache/log4j/config/Log4j1ConfigurationFactoryTest.java
+++ b/log4j-1.2-api/src/test/java/org/apache/log4j/config/Log4j1ConfigurationFactoryTest.java
@@ -169,7 +169,7 @@
             final Configuration configuration = getConfiguration("config-1.2/log4j-system-properties-1.properties");
             final RollingFileAppender appender = configuration.getAppender("RFA");
 			appender.stop(10, TimeUnit.SECONDS);
-            System.out.println("expected: " + tempFileName + " Actual: " + appender.getFileName());
+            // System.out.println("expected: " + tempFileName + " Actual: " + appender.getFileName());
             assertEquals(tempFileName, appender.getFileName());
         } finally {
 			try {
diff --git a/log4j-api-java9/src/main/java/org/apache/logging/log4j/util/StackLocator.java b/log4j-api-java9/src/main/java/org/apache/logging/log4j/util/StackLocator.java
index 12e4e2a..8cd6ae6 100644
--- a/log4j-api-java9/src/main/java/org/apache/logging/log4j/util/StackLocator.java
+++ b/log4j-api-java9/src/main/java/org/apache/logging/log4j/util/StackLocator.java
@@ -17,6 +17,7 @@
 package org.apache.logging.log4j.util;
 
 import java.util.List;
+import java.util.Optional;
 import java.util.Stack;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -33,9 +34,6 @@
 
     private final static StackLocator INSTANCE = new StackLocator();
 
-    private final static ThreadLocal<String> FQCN = new ThreadLocal<>();
-    private final static FqcnCallerLocator LOCATOR = new FqcnCallerLocator();
-
     public static StackLocator getInstance() {
         return INSTANCE;
     }
@@ -75,36 +73,14 @@
     }
 
     public StackTraceElement calcLocation(final String fqcnOfLogger) {
-        FQCN.set(fqcnOfLogger);
-        final StackWalker.StackFrame walk = walker.walk(LOCATOR);
-        final StackTraceElement element = walk == null ? null : walk.toStackTraceElement();
-        FQCN.set(null);
-        return element;
+        return stackWalker.walk(
+                s -> s.dropWhile(f -> !f.getClassName().equals(fqcnOfLogger)) // drop the top frames until we reach the logger
+                        .dropWhile(f -> f.getClassName().equals(fqcnOfLogger)) // drop the logger frames
+                        .findFirst()).map(StackWalker.StackFrame::toStackTraceElement).orElse(null);
     }
 
     public StackTraceElement getStackTraceElement(final int depth) {
-        return stackWalker.walk(s -> s.skip(depth).findFirst()).get().toStackTraceElement();
-    }
-
-    static final class FqcnCallerLocator implements Function<Stream<StackWalker.StackFrame>, StackWalker.StackFrame> {
-
-        @Override
-        public StackWalker.StackFrame apply(Stream<StackWalker.StackFrame> stackFrameStream) {
-            String fqcn = FQCN.get();
-            boolean foundFqcn = false;
-            Object[] frames = stackFrameStream.toArray();
-            for (int i = 0; i < frames.length ; ++i) {
-                final String className = ((StackWalker.StackFrame) frames[i]).getClassName();
-                if (!foundFqcn) {
-                    // Skip frames until we find the FQCN
-                    foundFqcn = className.equals(fqcn);
-                } else if (!className.equals(fqcn)) {
-                    // The frame is no longer equal to the FQCN so it is the one we want.
-                    return (StackWalker.StackFrame) frames[i];
-                } // Otherwise it is equal to the FQCN so we need to skip it.
-            }
-            // Should never happen
-            return null;
-        }
+        return stackWalker.walk(s -> s.skip(depth).findFirst())
+                .map(StackWalker.StackFrame::toStackTraceElement).orElse(null);
     }
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/StackLocatorUtil.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/StackLocatorUtil.java
index d4c0c64..3d51f1d 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/StackLocatorUtil.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/StackLocatorUtil.java
@@ -26,7 +26,7 @@
  */
 public final class StackLocatorUtil {
     private static StackLocator stackLocator = null;
-    private static volatile boolean errorLogged = false;
+    private static volatile boolean errorLogged;
 
     static {
         stackLocator = StackLocator.getInstance();
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 ea06dff..be50ab8 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
@@ -55,14 +55,23 @@
     }
 
     /**
-     * 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;
     }
 
     /**
@@ -286,4 +295,23 @@
         return buf.toString();
     }
 
+    /**
+     * 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 162d162..e91e298 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 @@
 
 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 ea845e5..0f21c78 100644
--- a/log4j-bom/pom.xml
+++ b/log4j-bom/pom.xml
@@ -72,6 +72,12 @@
         <artifactId>log4j-layout-jackson-yaml</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/LoggerContext.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
index 8b181d0..8b369fe 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
@@ -27,6 +27,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -43,6 +44,7 @@
 import org.apache.logging.log4j.core.config.NullConfiguration;
 import org.apache.logging.log4j.core.config.Reconfigurable;
 import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.core.impl.ThreadContextDataInjector;
 import org.apache.logging.log4j.core.jmx.Server;
 import org.apache.logging.log4j.core.util.Cancellable;
 import org.apache.logging.log4j.core.util.ExecutorServices;
@@ -59,7 +61,6 @@
 import org.apache.logging.log4j.spi.ThreadContextMapFactory;
 import org.apache.logging.log4j.util.PropertiesUtil;
 
-
 /**
  * The LoggerContext is the anchor for the logging system. It maintains a list of all the loggers requested by
  * applications and a reference to the Configuration. The Configuration will contain the configured loggers, appenders,
@@ -87,7 +88,7 @@
 
     private final LoggerRegistry<Logger> loggerRegistry = new LoggerRegistry<>();
     private final CopyOnWriteArrayList<PropertyChangeListener> propertyChangeListeners = new CopyOnWriteArrayList<>();
-    private volatile List<LoggerContextShutdownAware> listeners = null;
+    private volatile List<LoggerContextShutdownAware> listeners;
 
     /**
      * The Configuration is volatile to guarantee that initialization of the Configuration has completed before the
@@ -136,6 +137,7 @@
             externalMap.put(EXTERNAL_CONTEXT_KEY, externalContext);
         }
         this.configLocation = configLocn;
+        CompletableFuture.runAsync(ThreadContextDataInjector::initServiceProviders);
     }
 
     /**
@@ -164,6 +166,7 @@
         } else {
             configLocation = null;
         }
+        CompletableFuture.runAsync(ThreadContextDataInjector::initServiceProviders);
     }
 
     public void addShutdownListener(LoggerContextShutdownAware listener) {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/AsyncAppender.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/AsyncAppender.java
index 0ea5ae6..d4a270b 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/AsyncAppender.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/AsyncAppender.java
@@ -342,7 +342,7 @@
      */
     private class AsyncThread extends Log4jThread {
 
-        private volatile boolean shutdown = false;
+        private volatile boolean shutdown;
         private final List<AppenderControl> appenders;
         private final BlockingQueue<LogEvent> queue;
 
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/FailoverAppender.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/FailoverAppender.java
index 7cab755..72bf446 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/FailoverAppender.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/FailoverAppender.java
@@ -61,7 +61,7 @@
 
     private final long intervalNanos;
 
-    private volatile long nextCheckNanos = 0;
+    private volatile long nextCheckNanos;
 
     private FailoverAppender(final String name, final Filter filter, final String primary, final String[] failovers,
             final int intervalMillis, final Configuration config, final boolean ignoreExceptions,
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java
index 129b9a6..e95dc85 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingFileManager.java
@@ -66,8 +66,8 @@
     private final Log4jThreadFactory threadFactory = Log4jThreadFactory.createThreadFactory("RollingFileManager");
     private volatile TriggeringPolicy triggeringPolicy;
     private volatile RolloverStrategy rolloverStrategy;
-    private volatile boolean renameEmptyFiles = false;
-    private volatile boolean initialized = false;
+    private volatile boolean renameEmptyFiles;
+    private volatile boolean initialized;
     private volatile String fileName;
     private final boolean directWrite;
 
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingRandomAccessFileManager.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingRandomAccessFileManager.java
index ad7df84..7f7bf85 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingRandomAccessFileManager.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/rolling/RollingRandomAccessFileManager.java
@@ -22,6 +22,7 @@
 import java.io.RandomAccessFile;
 import java.io.Serializable;
 import java.nio.ByteBuffer;
+import java.nio.file.Paths;
 
 import org.apache.logging.log4j.core.Layout;
 import org.apache.logging.log4j.core.LoggerContext;
@@ -140,6 +141,9 @@
 
     private void createFileAfterRollover(String fileName) throws IOException {
         this.randomAccessFile = new RandomAccessFile(fileName, "rw");
+        if (isAttributeViewEnabled()) {
+            defineAttributeView(Paths.get(fileName));
+        }
         if (isAppend()) {
             randomAccessFile.seek(randomAccessFile.length());
         }
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/routing/RoutingAppender.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/routing/RoutingAppender.java
index 546f690..2d2e3d0 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/routing/RoutingAppender.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/routing/RoutingAppender.java
@@ -415,7 +415,7 @@
 
     private static final class CreatedRouteAppenderControl extends RouteAppenderControl {
 
-        private volatile boolean pendingDeletion = false;
+        private volatile boolean pendingDeletion;
         private final AtomicInteger depth = new AtomicInteger();
 
         CreatedRouteAppenderControl(Appender appender) {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfigDisruptor.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfigDisruptor.java
index be34e09..252cd28 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfigDisruptor.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerConfigDisruptor.java
@@ -27,6 +27,7 @@
 import org.apache.logging.log4j.core.impl.MutableLogEvent;
 import org.apache.logging.log4j.core.impl.ReusableLogEventFactory;
 import org.apache.logging.log4j.core.jmx.RingBufferAdmin;
+import org.apache.logging.log4j.core.util.Log4jThread;
 import org.apache.logging.log4j.core.util.Log4jThreadFactory;
 import org.apache.logging.log4j.core.util.Throwables;
 import org.apache.logging.log4j.message.ReusableMessage;
@@ -184,7 +185,7 @@
     private long backgroundThreadId; // LOG4J2-471
     private EventFactory<Log4jEventWrapper> factory;
     private EventTranslatorTwoArg<Log4jEventWrapper, LogEvent, AsyncLoggerConfig> translator;
-    private volatile boolean alreadyLoggedWarning = false;
+    private volatile boolean alreadyLoggedWarning;
 
     private final Object queueFullEnqueueLock = new Object();
 
@@ -383,7 +384,12 @@
     private boolean synchronizeEnqueueWhenQueueFull() {
         return DisruptorUtil.ASYNC_CONFIG_SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL
                 // Background thread must never block
-                && backgroundThreadId != Thread.currentThread().getId();
+                && backgroundThreadId != Thread.currentThread().getId()
+                // Threads owned by log4j are most likely to result in
+                // deadlocks because they generally consume events.
+                // This prevents deadlocks between AsyncLoggerContext
+                // disruptors.
+                && !(Thread.currentThread() instanceof Log4jThread);
     }
 
     @Override
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerDisruptor.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerDisruptor.java
index e8c0121..377100e 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerDisruptor.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/AsyncLoggerDisruptor.java
@@ -25,6 +25,7 @@
 import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.core.AbstractLifeCycle;
 import org.apache.logging.log4j.core.jmx.RingBufferAdmin;
+import org.apache.logging.log4j.core.util.Log4jThread;
 import org.apache.logging.log4j.core.util.Log4jThreadFactory;
 import org.apache.logging.log4j.core.util.Throwables;
 
@@ -85,6 +86,11 @@
                     contextName);
             return;
         }
+        if (isStarting()) {
+            LOGGER.trace("[{}] AsyncLoggerDisruptor is already starting.", contextName);
+            return;
+        }
+        setStarting();
         LOGGER.trace("[{}] AsyncLoggerDisruptor creating new disruptor for this context.", contextName);
         ringBufferSize = DisruptorUtil.calculateRingBufferSize("AsyncLogger.RingBufferSize");
         final WaitStrategy waitStrategy = DisruptorUtil.createWaitStrategy("AsyncLogger.WaitStrategy");
@@ -281,7 +287,12 @@
     private boolean synchronizeEnqueueWhenQueueFull() {
         return DisruptorUtil.ASYNC_LOGGER_SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL
                 // Background thread must never block
-                && backgroundThreadId != Thread.currentThread().getId();
+                && backgroundThreadId != Thread.currentThread().getId()
+                // Threads owned by log4j are most likely to result in
+                // deadlocks because they generally consume events.
+                // This prevents deadlocks between AsyncLoggerContext
+                // disruptors.
+                && !(Thread.currentThread() instanceof Log4jThread);
     }
 
     private void logWarningOnNpeFromDisruptorPublish(final RingBufferLogEventTranslator translator) {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DefaultAsyncQueueFullPolicy.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DefaultAsyncQueueFullPolicy.java
index 1d48113..d3d53e1 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DefaultAsyncQueueFullPolicy.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DefaultAsyncQueueFullPolicy.java
@@ -17,6 +17,7 @@
 package org.apache.logging.log4j.core.async;
 
 import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.util.Log4jThread;
 
 /**
  * Default router: enqueue the event for asynchronous logging in the background thread, unless the current thread is the
@@ -29,7 +30,13 @@
 
         // LOG4J2-471: prevent deadlock when RingBuffer is full and object
         // being logged calls Logger.log() from its toString() method
-        if (Thread.currentThread().getId() == backgroundThreadId) {
+        Thread currentThread = Thread.currentThread();
+        if (currentThread.getId() == backgroundThreadId
+                // Threads owned by log4j are most likely to result in
+                // deadlocks because they generally consume events.
+                // This prevents deadlocks between AsyncLoggerContext
+                // disruptors.
+                || currentThread instanceof Log4jThread) {
             return EventRoute.SYNCHRONOUS;
         }
         return EventRoute.ENQUEUE;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DisruptorUtil.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DisruptorUtil.java
index 0299541..5925187 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DisruptorUtil.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/DisruptorUtil.java
@@ -17,7 +17,6 @@
 
 package org.apache.logging.log4j.core.async;
 
-import java.util.Locale;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
@@ -36,6 +35,7 @@
 import org.apache.logging.log4j.core.util.Loader;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.Strings;
 
 /**
  * Utility methods for getting Disruptor related configuration.
@@ -60,36 +60,46 @@
     private DisruptorUtil() {
     }
 
-    static long getTimeout(final String propertyName, final long defaultTimeout) {
-        return PropertiesUtil.getProperties().getLongProperty(propertyName, defaultTimeout);
-    }
-
     static WaitStrategy createWaitStrategy(final String propertyName) {
-        final String key = propertyName.startsWith("AsyncLogger.")
-                ? "AsyncLogger.Timeout"
-                : "AsyncLoggerConfig.Timeout";
-        final long timeoutMillis = DisruptorUtil.getTimeout(key, 10L);
-        return createWaitStrategy(propertyName, timeoutMillis);
+        final String strategy = PropertiesUtil.getProperties().getStringProperty(propertyName, "Timeout");
+        LOGGER.trace("property {}={}", propertyName, strategy);
+        final String strategyUp = Strings.toRootUpperCase(strategy);
+        final long timeoutMillis = parseAdditionalLongProperty(propertyName, "Timeout", 10L);
+        // String (not enum) is deliberately used here to avoid IllegalArgumentException being thrown. In case of
+        // incorrect property value, default WaitStrategy is created.
+        switch (strategyUp) {
+            case "SLEEP":
+                final long sleepTimeNs =
+                        parseAdditionalLongProperty(propertyName, "SleepTimeNs", 100L);
+                final String key = getFullPropertyKey(propertyName, "Retries");
+                final int retries =
+                        PropertiesUtil.getProperties().getIntegerProperty(key, 200);
+                return new SleepingWaitStrategy(retries, sleepTimeNs);
+            case "YIELD":
+                return new YieldingWaitStrategy();
+            case "BLOCK":
+                return new BlockingWaitStrategy();
+            case "BUSYSPIN":
+                return new BusySpinWaitStrategy();
+            case "TIMEOUT":
+                return new TimeoutBlockingWaitStrategy(timeoutMillis, TimeUnit.MILLISECONDS);
+            default:
+                return new TimeoutBlockingWaitStrategy(timeoutMillis, TimeUnit.MILLISECONDS);
+        }
     }
 
-    static WaitStrategy createWaitStrategy(final String propertyName, final long timeoutMillis) {
-        final String strategy = PropertiesUtil.getProperties().getStringProperty(propertyName, "TIMEOUT");
-        LOGGER.trace("property {}={}", propertyName, strategy);
-        final String strategyUp = strategy.toUpperCase(Locale.ROOT); // TODO Refactor into Strings.toRootUpperCase(String)
-        switch (strategyUp) { // TODO Define a DisruptorWaitStrategy enum?
-        case "SLEEP":
-            return new SleepingWaitStrategy();
-        case "YIELD":
-            return new YieldingWaitStrategy();
-        case "BLOCK":
-            return new BlockingWaitStrategy();
-        case "BUSYSPIN":
-            return new BusySpinWaitStrategy();
-        case "TIMEOUT":
-            return new TimeoutBlockingWaitStrategy(timeoutMillis, TimeUnit.MILLISECONDS);
-        default:
-            return new TimeoutBlockingWaitStrategy(timeoutMillis, TimeUnit.MILLISECONDS);
-        }
+    private static String getFullPropertyKey(final String strategyKey, final String additionalKey) {
+        return strategyKey.startsWith("AsyncLogger.")
+                ? "AsyncLogger." + additionalKey
+                : "AsyncLoggerConfig." + additionalKey;
+    }
+
+    private static long parseAdditionalLongProperty(
+            final String propertyName,
+            final String additionalKey,
+            long defaultValue) {
+        final String key = getFullPropertyKey(propertyName, additionalKey);
+        return PropertiesUtil.getProperties().getLongProperty(key, defaultValue);
     }
 
     static int calculateRingBufferSize(final String propertyName) {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java
index c777cae..c536c04 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/async/RingBufferLogEventTranslator.java
@@ -37,7 +37,7 @@
 public class RingBufferLogEventTranslator implements
         EventTranslator<RingBufferLogEvent> {
 
-    private final ContextDataInjector injector = ContextDataInjectorFactory.createInjector();
+    private static final ContextDataInjector INJECTOR = ContextDataInjectorFactory.createInjector();
     private AsyncLogger asyncLogger;
     String loggerName;
     protected Marker marker;
@@ -60,7 +60,7 @@
         event.setValues(asyncLogger, loggerName, marker, fqcn, level, message, thrown,
                 // config properties are taken care of in the EventHandler thread
                 // in the AsyncLogger#actualAsyncLog method
-                injector.injectContextData(null, (StringMap) event.getContextData()), contextStack,
+                INJECTOR.injectContextData(null, (StringMap) event.getContextData()), contextStack,
                 threadId, threadName, threadPriority, location, clock, nanoClock);
 
         clear(); // clear the translator
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java
index 2db7a2d..7112f56 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationFactory.java
@@ -138,7 +138,7 @@
 
     private static final String OVERRIDE_PARAM = "override";
 
-    private static volatile List<ConfigurationFactory> factories = null;
+    private static volatile List<ConfigurationFactory> factories;
 
     private static ConfigurationFactory configFactory = new Factory();
 
@@ -149,7 +149,7 @@
     private static final String HTTPS = "https";
     private static final String HTTP = "http";
 
-    private static volatile AuthorizationProvider authorizationProvider = null;
+    private static volatile AuthorizationProvider authorizationProvider;
 
     /**
      * Returns the ConfigurationFactory.
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationScheduler.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationScheduler.java
index 5341337..e393d4c 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationScheduler.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationScheduler.java
@@ -38,8 +38,8 @@
     private static final Logger LOGGER = StatusLogger.getLogger();
     private static final String SIMPLE_NAME = "Log4j2 " + ConfigurationScheduler.class.getSimpleName();
     private static final int MAX_SCHEDULED_ITEMS = 5;
-    
-    private ScheduledExecutorService executorService;
+
+    private volatile ScheduledExecutorService executorService;
     private int scheduledItems = 0;
     private final String name;
 
@@ -193,17 +193,21 @@
 
     private ScheduledExecutorService getExecutorService() {
         if (executorService == null) {
-            if (scheduledItems > 0) {
-                LOGGER.debug("{} starting {} threads", name, scheduledItems);
-                scheduledItems = Math.min(scheduledItems, MAX_SCHEDULED_ITEMS);
-                final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(scheduledItems,
-                        Log4jThreadFactory.createDaemonThreadFactory("Scheduled"));
-                executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
-                executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
-                this.executorService = executor;
+            synchronized (this) {
+                if (executorService == null) {
+                    if (scheduledItems > 0) {
+                        LOGGER.debug("{} starting {} threads", name, scheduledItems);
+                        scheduledItems = Math.min(scheduledItems, MAX_SCHEDULED_ITEMS);
+                        final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(scheduledItems,
+                                Log4jThreadFactory.createDaemonThreadFactory("Scheduled"));
+                        executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
+                        executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
+                        this.executorService = executor;
 
-            } else {
-                LOGGER.debug("{}: No scheduled items", name);
+                    } else {
+                        LOGGER.debug("{}: No scheduled items", name);
+                    }
+                }
             }
         }
         return executorService;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationSource.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationSource.java
index 087d447..ad94891 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationSource.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/ConfigurationSource.java
@@ -59,7 +59,7 @@
     private final String location;
     private final InputStream stream;
     private volatile byte[] data;
-    private volatile Source source = null;
+    private volatile Source source;
     private final long lastModified;
     // Set when the configuration has been updated so reset can use it for the next lastModified timestamp.
     private volatile long modifiedMillis;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/LockingReliabilityStrategy.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/LockingReliabilityStrategy.java
index 391f2ec..21e3557 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/LockingReliabilityStrategy.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/LockingReliabilityStrategy.java
@@ -33,7 +33,7 @@
 public class LockingReliabilityStrategy implements ReliabilityStrategy {
     private final LoggerConfig loggerConfig;
     private final ReadWriteLock reconfigureLock = new ReentrantReadWriteLock();
-    private volatile boolean isStopping = false;
+    private volatile boolean isStopping;
 
     public LockingReliabilityStrategy(final LoggerConfig loggerConfig) {
         this.loggerConfig = Objects.requireNonNull(loggerConfig, "loggerConfig was null");
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/properties/PropertiesConfigurationBuilder.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/properties/PropertiesConfigurationBuilder.java
index 6d52329..41d274f 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/properties/PropertiesConfigurationBuilder.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/properties/PropertiesConfigurationBuilder.java
@@ -228,7 +228,7 @@
     private FilterComponentBuilder createFilter(final String key, final Properties properties) {
         final String type = (String) properties.remove(CONFIG_TYPE);
         if (Strings.isEmpty(type)) {
-            throw new ConfigurationException("No type attribute provided for Appender " + key);
+            throw new ConfigurationException("No type attribute provided for Filter " + key);
         }
         final String onMatch = (String) properties.remove(AbstractFilterBuilder.ATTR_ON_MATCH);
         final String onMismatch = (String) properties.remove(AbstractFilterBuilder.ATTR_ON_MISMATCH);
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/status/StatusConfiguration.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/status/StatusConfiguration.java
index 1ad67a3..6070d30 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/status/StatusConfiguration.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/status/StatusConfiguration.java
@@ -47,7 +47,7 @@
     private final Collection<String> errorMessages = new LinkedBlockingQueue<String>();
     private final StatusLogger logger = StatusLogger.getLogger();
 
-    private volatile boolean initialized = false;
+    private volatile boolean initialized;
 
     private PrintStream destination = DEFAULT_STREAM;
     private Level status = DEFAULT_STATUS;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
index 0522eca..ace66ef 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.java
@@ -23,6 +23,8 @@
 import java.util.Map;
 import java.util.ServiceLoader;
 import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.ThreadContext;
@@ -60,6 +62,39 @@
     public static Collection<ContextDataProvider> contextDataProviders =
             new ConcurrentLinkedDeque<>();
 
+    private static volatile List<ContextDataProvider> serviceProviders = null;
+    private static final Lock providerLock = new ReentrantLock();
+
+    public static void initServiceProviders() {
+        if (serviceProviders == null) {
+            providerLock.lock();
+            try {
+                if (serviceProviders == null) {
+                    serviceProviders = getServiceProviders();
+                }
+            } finally {
+                providerLock.unlock();
+            }
+        }
+    }
+
+    private static List<ContextDataProvider> getServiceProviders() {
+        List<ContextDataProvider> providers = new ArrayList<>();
+        for (final ClassLoader classLoader : LoaderUtil.getClassLoaders()) {
+            try {
+                for (final ContextDataProvider provider : ServiceLoader.load(ContextDataProvider.class, classLoader)) {
+                    if (providers.stream().noneMatch((p) -> p.getClass().isAssignableFrom(provider.getClass()))) {
+                        providers.add(provider);
+                    }
+                }
+            } catch (final Throwable ex) {
+                LOGGER.debug("Unable to access Context Data Providers {}", ex.getMessage());
+            }
+        }
+        return providers;
+    }
+
+
     /**
      * Default {@code ContextDataInjector} for the legacy {@code Map<String, String>}-based ThreadContext (which is
      * also the ThreadContext implementation used for web applications).
@@ -208,7 +243,7 @@
                 // this will replace the LogEvent's context data with the returned instance
                 return providers.get(0).supplyStringMap();
             }
-            int count = props.size();
+            int count = props == null ? 0 : props.size();
             StringMap[] maps = new StringMap[providers.size()];
             for (int i = 0; i < providers.size(); ++i) {
                 maps[i] = providers.get(i).supplyStringMap();
@@ -248,17 +283,10 @@
     }
 
     private static List<ContextDataProvider> getProviders() {
+        initServiceProviders();
         final List<ContextDataProvider> providers = new ArrayList<>(contextDataProviders);
-        for (final ClassLoader classLoader : LoaderUtil.getClassLoaders()) {
-            try {
-                for (final ContextDataProvider provider : ServiceLoader.load(ContextDataProvider.class, classLoader)) {
-                    if (providers.stream().noneMatch((p) -> p.getClass().isAssignableFrom(provider.getClass()))) {
-                        providers.add(provider);
-                    }
-                }
-            } catch (final Throwable ex) {
-                LOGGER.debug("Unable to access Context Data Providers {}", ex.getMessage());
-            }
+        if (serviceProviders != null) {
+            providers.addAll(serviceProviders);
         }
         return providers;
     }
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormat.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormat.java
index e169ffc..4316a59 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormat.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/time/internal/format/FixedDateFormat.java
@@ -392,8 +392,8 @@
     private final FixedTimeZoneFormat fixedTimeZoneFormat;
 
 
-    private volatile long midnightToday = 0;
-    private volatile long midnightTomorrow = 0;
+    private volatile long midnightToday;
+    private volatile long midnightTomorrow;
     private final int[] dstOffsets = new int[25];
 
     // cachedDate does not need to be volatile because
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ArrayUtils.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ArrayUtils.java
index bcf8d54..c71a166 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ArrayUtils.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/ArrayUtils.java
@@ -52,6 +52,17 @@
     }
 
     /**
+     * Checks if an array of Objects is empty or {@code null}.
+     *
+     * @param array  the array to test
+     * @return {@code true} if the array is empty or {@code null}
+     * @since 2.1
+     */
+    public static boolean isEmpty(final byte[] array) {
+        return getLength(array) == 0;
+    }    
+
+    /**
      * <p>Removes the element at the specified position from the specified array.
      * All subsequent elements are shifted to the left (subtracts one from
      * their indices).</p>
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/NetUtils.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/NetUtils.java
index e848822..195dbff 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/NetUtils.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/NetUtils.java
@@ -52,19 +52,21 @@
     public static String getLocalHostname() {
         try {
             final InetAddress addr = InetAddress.getLocalHost();
-            return addr.getHostName();
+            return addr == null ? UNKNOWN_LOCALHOST : addr.getHostName();
         } catch (final UnknownHostException uhe) {
             try {
                 final Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
-                while (interfaces.hasMoreElements()) {
-                    final NetworkInterface nic = interfaces.nextElement();
-                    final Enumeration<InetAddress> addresses = nic.getInetAddresses();
-                    while (addresses.hasMoreElements()) {
-                        final InetAddress address = addresses.nextElement();
-                        if (!address.isLoopbackAddress()) {
-                            final String hostname = address.getHostName();
-                            if (hostname != null) {
-                                return hostname;
+                if (interfaces != null) {
+                    while (interfaces.hasMoreElements()) {
+                        final NetworkInterface nic = interfaces.nextElement();
+                        final Enumeration<InetAddress> addresses = nic.getInetAddresses();
+                        while (addresses.hasMoreElements()) {
+                            final InetAddress address = addresses.nextElement();
+                            if (!address.isLoopbackAddress()) {
+                                final String hostname = address.getHostName();
+                                if (hostname != null) {
+                                    return hostname;
+                                }
                             }
                         }
                     }
@@ -95,18 +97,20 @@
                 }
                 if (mac == null) {
                     final Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
-                    while (networkInterfaces.hasMoreElements() && mac == null) {
-                        final NetworkInterface nic = networkInterfaces.nextElement();
-                        if (isUpAndNotLoopback(nic)) {
-                            mac = nic.getHardwareAddress();
+                    if (networkInterfaces != null) {
+                        while (networkInterfaces.hasMoreElements() && mac == null) {
+                            final NetworkInterface nic = networkInterfaces.nextElement();
+                            if (isUpAndNotLoopback(nic)) {
+                                mac = nic.getHardwareAddress();
+                            }
                         }
                     }
                 }
             } catch (final SocketException e) {
                 LOGGER.catching(e);
             }
-            if (mac == null || mac.length == 0) {
-                // Emulate a mac address with an IP v4 or v6
+            if (ArrayUtils.isEmpty(mac) && localHost != null) {
+                // Emulate a MAC address with an IP v4 or v6
                 final byte[] address = localHost.getAddress();
                 // Take only 6 bytes if the address is an IPv6 otherwise will pad with two zero bytes
                 mac = Arrays.copyOf(address, 6);
@@ -123,7 +127,7 @@
      */
     public static String getMacAddressString() {
         final byte[] macAddr = getMacAddress();
-        if (macAddr != null && macAddr.length > 0) {
+        if (!ArrayUtils.isEmpty(macAddr)) {
             StringBuilder sb = new StringBuilder(String.format("%02x", macAddr[0]));
             for (int i = 1; i < macAddr.length; ++i) {
                 sb.append(":").append(String.format("%02x", macAddr[i]));
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 @@
      * @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/main/java/org/apache/logging/log4j/core/util/WatcherFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/WatcherFactory.java
index dc88e56..31acd5a 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/WatcherFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/WatcherFactory.java
@@ -39,7 +39,7 @@
     private static final Logger LOGGER = StatusLogger.getLogger();
     private static final PluginManager pluginManager = new PluginManager(Watcher.CATEGORY);
 
-    private static volatile WatcherFactory factory = null;
+    private static volatile WatcherFactory factory;
 
     private final Map<String, PluginType<?>> plugins;
 
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 46efe64..938234f 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 @@
         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 -> {
 
-        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"));
-        }
+            // 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"));
+            }
+
+        });
+
     }
 
     private static File agentJar() throws Exception {
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/LoggingTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/LoggingTest.java
new file mode 100644
index 0000000..1365a08
--- /dev/null
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/LoggingTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.core;
+
+import java.util.List;
+
+import org.apache.logging.log4j.junit.LoggerContextRule;
+import org.apache.logging.log4j.test.appender.ListAppender;
+import org.apache.logging.log4j.util.Timer;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+import static org.junit.Assert.assertEquals;
+
+
+public class LoggingTest {
+
+    private static final String CONFIG = "log4j-list.xml";
+
+    @Rule
+    public final TestName testName = new TestName();
+    private ListAppender list;
+
+
+    @Rule
+    public LoggerContextRule context = new LoggerContextRule(CONFIG);
+
+    private void assertEventCount(final List<LogEvent> events, final int expected) {
+        assertEquals("Incorrect number of events.", expected, events.size());
+    }
+
+    @Before
+    public void before() {
+        logger = context.getLogger("LoggerTest");
+    }
+
+    org.apache.logging.log4j.Logger logger;
+
+    @Test
+    public void logTime() {
+        Timer timer = new Timer("initial");
+        timer.start();
+        logger.info("This is a test");
+        System.out.println(timer.stop());
+        timer = new Timer("more", 100);
+/*        timer.start();
+        for (int i=0; i < 100; ++i) {
+            logger.info("This is another test");
+        }
+        System.out.println(timer.stop());*/
+    }
+}
+
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingRandomAccessFileManagerTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingRandomAccessFileManagerTest.java
index 8f7f717..b365322 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingRandomAccessFileManagerTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/rolling/RollingRandomAccessFileManagerTest.java
@@ -17,6 +17,26 @@
 
 package org.apache.logging.log4j.core.appender.rolling;
 
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.util.Closer;
+import org.apache.logging.log4j.core.util.FileUtils;
+import org.apache.logging.log4j.core.util.NullOutputStream;
+import org.apache.logging.log4j.util.Strings;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.Set;
+import java.util.concurrent.locks.LockSupport;
+
 import static org.apache.logging.log4j.hamcrest.FileMatchers.beforeNow;
 import static org.apache.logging.log4j.hamcrest.FileMatchers.hasLength;
 import static org.apache.logging.log4j.hamcrest.FileMatchers.isEmpty;
@@ -25,21 +45,10 @@
 import static org.hamcrest.Matchers.lessThanOrEqualTo;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.RandomAccessFile;
-import java.util.concurrent.locks.LockSupport;
-
-import org.apache.logging.log4j.core.util.Closer;
-import org.apache.logging.log4j.core.util.NullOutputStream;
-import org.apache.logging.log4j.util.Strings;
-import org.junit.Test;
-
 /**
  * Tests the RollingRandomAccessFileManager class.
  */
@@ -198,4 +207,63 @@
         assertThat(file, lastModified(equalTo(manager.getFileTime())));
     }
 
+    @Test
+    public void testRolloverRetainsFileAttributes() throws Exception {
+
+        // Short-circuit if host doesn't support file attributes.
+        if (!FileUtils.isFilePosixAttributeViewSupported()) {
+            return;
+        }
+
+        // Create the initial file.
+        final File file = File.createTempFile("log4j2", "test");
+        LockSupport.parkNanos(1000000); // 1 millisec
+
+        // Set the initial file attributes.
+        final String filePermissionsString = "rwxrwxrwx";
+        final Set<PosixFilePermission> filePermissions =
+                PosixFilePermissions.fromString(filePermissionsString);
+        FileUtils.defineFilePosixAttributeView(file.toPath(), filePermissions, null, null);
+
+        // Create the manager.
+        final RolloverStrategy rolloverStrategy = DefaultRolloverStrategy
+                .newBuilder()
+                .setMax("7")
+                .setMin("1")
+                .setFileIndex("max")
+                .setStopCustomActionsOnError(false)
+                .setConfig(new DefaultConfiguration())
+                .build();
+        final RollingRandomAccessFileManager manager =
+                RollingRandomAccessFileManager.getRollingRandomAccessFileManager(
+                        file.getAbsolutePath(),
+                        Strings.EMPTY,
+                        true,
+                        true,
+                        RollingRandomAccessFileManager.DEFAULT_BUFFER_SIZE,
+                        new SizeBasedTriggeringPolicy(Long.MAX_VALUE),
+                        rolloverStrategy,
+                        null,
+                        null,
+                        filePermissionsString,
+                        null,
+                        null,
+                        null);
+        assertNotNull(manager);
+        manager.initialize();
+
+        // Trigger a rollover.
+        manager.rollover();
+
+        // Verify the rolled over file attributes.
+        final Set<PosixFilePermission> actualFilePermissions = Files
+                .getFileAttributeView(
+                        Paths.get(manager.getFileName()),
+                        PosixFileAttributeView.class)
+                .readAttributes()
+                .permissions();
+        assertEquals(filePermissions, actualFilePermissions);
+
+    }
+
 }
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/InitTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/InitTest.java
index 453fa19..5744886 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/InitTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/InitTest.java
@@ -16,16 +16,18 @@
  */
 package org.apache.logging.log4j.core.util;
 
+import static org.junit.Assert.assertTrue;
+
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.util.Timer;
+import org.junit.Ignore;
 import org.junit.Test;
 
-import static org.junit.Assert.assertTrue;
-
 /**
  * Test initialization.
  */
+@Ignore
 public class InitTest {
 
     @Test
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/NetUtilsTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/NetUtilsTest.java
index c624798..fc40037 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/util/NetUtilsTest.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/util/NetUtilsTest.java
@@ -32,7 +32,7 @@
     private static final boolean IS_WINDOWS = PropertiesUtil.getProperties().isOsWindows();

 

     @Test

-    public void testToUriWithoutBackslashes() throws URISyntaxException {

+    public void testToUriWithoutBackslashes() {

         final String config = "file:///path/to/something/on/unix";

         final URI uri = NetUtils.toURI(config);

 

@@ -41,7 +41,7 @@
     }

 

     @Test

-    public void testToUriWindowsWithBackslashes() throws URISyntaxException {

+    public void testToUriWindowsWithBackslashes() {

         Assume.assumeTrue(IS_WINDOWS);

         final String config = "file:///D:\\path\\to\\something/on/windows";

         final URI uri = NetUtils.toURI(config);

@@ -51,7 +51,7 @@
     }

 

     @Test

-    public void testToUriWindowsAbsolutePath() throws URISyntaxException {

+    public void testToUriWindowsAbsolutePath() {

         Assume.assumeTrue(IS_WINDOWS);

         final String config = "D:\\path\\to\\something\\on\\windows";

         final URI uri = NetUtils.toURI(config);

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-docker/src/main/java/org/apache/logging/log4j/docker/DockerLookup.java b/log4j-docker/src/main/java/org/apache/logging/log4j/docker/DockerLookup.java
index cd2c1cb..061376b 100644
--- a/log4j-docker/src/main/java/org/apache/logging/log4j/docker/DockerLookup.java
+++ b/log4j-docker/src/main/java/org/apache/logging/log4j/docker/DockerLookup.java
@@ -53,6 +53,8 @@
         }
         if (baseUri == null) {
             LOGGER.warn("No Docker URI provided. Docker information is unavailable");
+            container = null;
+            return;
         }
         Container current = null;
         try {
diff --git a/log4j-flume-ng/src/main/java/org/apache/logging/log4j/flume/appender/FlumeAppender.java b/log4j-flume-ng/src/main/java/org/apache/logging/log4j/flume/appender/FlumeAppender.java
index a7aa460..c2e7a2e 100644
--- a/log4j-flume-ng/src/main/java/org/apache/logging/log4j/flume/appender/FlumeAppender.java
+++ b/log4j-flume-ng/src/main/java/org/apache/logging/log4j/flume/appender/FlumeAppender.java
@@ -63,7 +63,7 @@
     private final FlumeEventFactory factory;
 
     private final Timer timer = new Timer("FlumeEvent", 5000);
-    private volatile long count = 0;
+    private volatile long count;
 
     /**
      * Which Manager will be used by the appender instance.
diff --git a/log4j-flume-ng/src/main/java/org/apache/logging/log4j/flume/appender/FlumeAvroManager.java b/log4j-flume-ng/src/main/java/org/apache/logging/log4j/flume/appender/FlumeAvroManager.java
index ad35b38..1dac698 100644
--- a/log4j-flume-ng/src/main/java/org/apache/logging/log4j/flume/appender/FlumeAvroManager.java
+++ b/log4j-flume-ng/src/main/java/org/apache/logging/log4j/flume/appender/FlumeAvroManager.java
@@ -50,7 +50,7 @@
 

     private final int current = 0;

 

-    private volatile RpcClient rpcClient = null;

+    private volatile RpcClient rpcClient;

 

     private BatchEvent batchEvent = new BatchEvent();

     private long nextSend = 0;

diff --git a/log4j-flume-ng/src/main/java/org/apache/logging/log4j/flume/appender/FlumePersistentManager.java b/log4j-flume-ng/src/main/java/org/apache/logging/log4j/flume/appender/FlumePersistentManager.java
index d15101f..7a989c0 100644
--- a/log4j-flume-ng/src/main/java/org/apache/logging/log4j/flume/appender/FlumePersistentManager.java
+++ b/log4j-flume-ng/src/main/java/org/apache/logging/log4j/flume/appender/FlumePersistentManager.java
@@ -469,7 +469,7 @@
      * Thread that sends data to Flume and pulls it from Berkeley DB.
      */
     private static class WriterThread extends Log4jThread  {
-        private volatile boolean shutdown = false;
+        private volatile boolean shutdown;
         private final Database database;
         private final Environment environment;
         private final FlumePersistentManager manager;
diff --git a/log4j-jdbc/src/main/java/org/apache/logging/log4j/jdbc/appender/JdbcDatabaseManager.java b/log4j-jdbc/src/main/java/org/apache/logging/log4j/jdbc/appender/JdbcDatabaseManager.java
index 122984a..4e9239d 100644
--- a/log4j-jdbc/src/main/java/org/apache/logging/log4j/jdbc/appender/JdbcDatabaseManager.java
+++ b/log4j-jdbc/src/main/java/org/apache/logging/log4j/jdbc/appender/JdbcDatabaseManager.java
@@ -161,7 +161,7 @@
     private final class Reconnector extends Log4jThread {
 
         private final CountDownLatch latch = new CountDownLatch(1);
-        private volatile boolean shutdown = false;
+        private volatile boolean shutdown;
 
         private Reconnector() {
             super("JdbcDatabaseManager-Reconnector");
diff --git a/log4j-jms/src/main/java/org/apache/logging/log4j/jms/appender/JmsManager.java b/log4j-jms/src/main/java/org/apache/logging/log4j/jms/appender/JmsManager.java
index 93d27b2..1ed2ba5 100644
--- a/log4j-jms/src/main/java/org/apache/logging/log4j/jms/appender/JmsManager.java
+++ b/log4j-jms/src/main/java/org/apache/logging/log4j/jms/appender/JmsManager.java
@@ -141,7 +141,7 @@
 
         private final CountDownLatch latch = new CountDownLatch(1);
 
-        private volatile boolean shutdown = false;
+        private volatile boolean shutdown;
 
         private final Object owner;
 
diff --git a/log4j-jul/pom.xml b/log4j-jul/pom.xml
index 3fa00b9..64d7040 100644
--- a/log4j-jul/pom.xml
+++ b/log4j-jul/pom.xml
@@ -60,6 +60,12 @@
       <artifactId>junit</artifactId>
       <scope>test</scope>
     </dependency>
+    <!-- Required for AsyncLogger testing -->
+    <dependency>
+      <groupId>com.lmax</groupId>
+      <artifactId>disruptor</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/log4j-jul/src/main/java/org/apache/logging/log4j/jul/ApiLogger.java b/log4j-jul/src/main/java/org/apache/logging/log4j/jul/ApiLogger.java
index 53e0b32..96512e6 100644
--- a/log4j-jul/src/main/java/org/apache/logging/log4j/jul/ApiLogger.java
+++ b/log4j-jul/src/main/java/org/apache/logging/log4j/jul/ApiLogger.java
@@ -119,22 +119,38 @@
 
     @Override
     public void log(final Level level, final String msg) {
-        logger.log(LevelTranslator.toLevel(level), msg);
+        if (getFilter() == null) {
+            logger.log(LevelTranslator.toLevel(level), msg);
+        } else {
+            super.log(level, msg);
+        }
     }
 
     @Override
     public void log(final Level level, final String msg, final Object param1) {
-        logger.log(LevelTranslator.toLevel(level), msg, param1);
+        if (getFilter() == null) {
+            logger.log(LevelTranslator.toLevel(level), msg, param1);
+        } else {
+            super.log(level, msg, param1);
+        }
     }
 
     @Override
     public void log(final Level level, final String msg, final Object[] params) {
-        logger.log(LevelTranslator.toLevel(level), msg, params);
+        if (getFilter() == null) {
+            logger.log(LevelTranslator.toLevel(level), msg, params);
+        } else {
+            super.log(level, msg, params);
+        }
     }
 
     @Override
     public void log(final Level level, final String msg, final Throwable thrown) {
-        logger.log(LevelTranslator.toLevel(level), msg, thrown);
+        if (getFilter() == null) {
+            logger.log(LevelTranslator.toLevel(level), msg, thrown);
+        } else {
+            super.log(level, msg, thrown);
+        }
     }
 
     @Override
@@ -216,36 +232,64 @@
 
     @Override
     public void severe(final String msg) {
-        logger.logIfEnabled(FQCN, org.apache.logging.log4j.Level.ERROR, null, msg);
+        if (getFilter() == null) {
+            logger.logIfEnabled(FQCN, org.apache.logging.log4j.Level.ERROR, null, msg);
+        } else {
+            super.severe(msg);
+        }
     }
 
     @Override
     public void warning(final String msg) {
-        logger.logIfEnabled(FQCN, org.apache.logging.log4j.Level.WARN, null, msg);
+        if (getFilter() == null) {
+            logger.logIfEnabled(FQCN, org.apache.logging.log4j.Level.WARN, null, msg);
+        } else {
+            super.warning(msg);
+        }
     }
 
     @Override
     public void info(final String msg) {
-        logger.logIfEnabled(FQCN, org.apache.logging.log4j.Level.INFO, null, msg);
+        if (getFilter() == null) {
+            logger.logIfEnabled(FQCN, org.apache.logging.log4j.Level.INFO, null, msg);
+        } else {
+            super.info(msg);
+        }
     }
 
     @Override
     public void config(final String msg) {
-        logger.logIfEnabled(FQCN, LevelTranslator.CONFIG, null, msg);
+        if (getFilter() == null) {
+            logger.logIfEnabled(FQCN, LevelTranslator.CONFIG, null, msg);
+        } else {
+            super.config(msg);
+        }
     }
 
     @Override
     public void fine(final String msg) {
-        logger.logIfEnabled(FQCN, org.apache.logging.log4j.Level.DEBUG, null, msg);
+        if (getFilter() == null) {
+            logger.logIfEnabled(FQCN, org.apache.logging.log4j.Level.DEBUG, null, msg);
+        } else {
+            super.fine(msg);
+        }
     }
 
     @Override
     public void finer(final String msg) {
-        logger.logIfEnabled(FQCN, org.apache.logging.log4j.Level.TRACE, null, msg);
+        if (getFilter() == null) {
+            logger.logIfEnabled(FQCN, org.apache.logging.log4j.Level.TRACE, null, msg);
+        } else {
+            super.finer(msg);
+        }
     }
 
     @Override
     public void finest(final String msg) {
-        logger.logIfEnabled(FQCN, LevelTranslator.FINEST, null, msg);
+        if (getFilter() == null) {
+            logger.logIfEnabled(FQCN, LevelTranslator.FINEST, null, msg);
+        } else {
+            super.finest(msg);
+        }
     }
 }
diff --git a/log4j-jul/src/test/java/org/apache/logging/log4j/jul/AbstractLoggerTest.java b/log4j-jul/src/test/java/org/apache/logging/log4j/jul/AbstractLoggerTest.java
index f81d3fa..ef69728 100644
--- a/log4j-jul/src/test/java/org/apache/logging/log4j/jul/AbstractLoggerTest.java
+++ b/log4j-jul/src/test/java/org/apache/logging/log4j/jul/AbstractLoggerTest.java
@@ -85,6 +85,34 @@
     }
 
     @Test
+    public void testLogFilter() throws Exception {
+        logger.setFilter(record -> false);
+        logger.severe("Informative message here.");
+        logger.warning("Informative message here.");
+        logger.info("Informative message here.");
+        logger.config("Informative message here.");
+        logger.fine("Informative message here.");
+        logger.finer("Informative message here.");
+        logger.finest("Informative message here.");
+        final List<LogEvent> events = eventAppender.getEvents();
+        assertThat(events, hasSize(0));
+    }
+
+    @Test
+    public void testAlteringLogFilter() throws Exception {
+        logger.setFilter(record -> { record.setMessage("This is not the message you are looking for."); return true; });
+        logger.info("Informative message here.");
+        final List<LogEvent> events = eventAppender.getEvents();
+        assertThat(events, hasSize(1));
+        final LogEvent event = events.get(0);
+        assertThat(event, instanceOf(Log4jLogEvent.class));
+        assertEquals(Level.INFO, event.getLevel());
+        assertEquals(LOGGER_NAME, event.getLoggerName());
+        assertEquals("This is not the message you are looking for.", event.getMessage().getFormattedMessage());
+        assertEquals(ApiLogger.class.getName(), event.getLoggerFqcn());
+    }
+
+    @Test
     public void testLogParamMarkers() {
         final Logger flowLogger = Logger.getLogger("TestFlow");
         flowLogger.logp(java.util.logging.Level.FINER, "sourceClass", "sourceMethod", "ENTER {0}", "params");
diff --git a/log4j-jul/src/test/java/org/apache/logging/log4j/jul/ApiLoggerTest.java b/log4j-jul/src/test/java/org/apache/logging/log4j/jul/ApiLoggerTest.java
index 6e08b3a..0f2c830 100644
--- a/log4j-jul/src/test/java/org/apache/logging/log4j/jul/ApiLoggerTest.java
+++ b/log4j-jul/src/test/java/org/apache/logging/log4j/jul/ApiLoggerTest.java
@@ -48,6 +48,7 @@
     @Before
     public void setUp() throws Exception {
         logger = Logger.getLogger(LOGGER_NAME);
+        logger.setFilter(null);
         assertThat(logger.getLevel(), equalTo(java.util.logging.Level.FINE));
         eventAppender = ListAppender.getListAppender("TestAppender");
         flowAppender = ListAppender.getListAppender("FlowAppender");
diff --git a/log4j-jul/src/test/java/org/apache/logging/log4j/jul/AsyncLoggerThreadsTest.java b/log4j-jul/src/test/java/org/apache/logging/log4j/jul/AsyncLoggerThreadsTest.java
new file mode 100644
index 0000000..3ca72e6
--- /dev/null
+++ b/log4j-jul/src/test/java/org/apache/logging/log4j/jul/AsyncLoggerThreadsTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.jul;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.categories.AsyncLoggers;
+import org.apache.logging.log4j.core.CoreLoggerContexts;
+import org.apache.logging.log4j.core.async.AsyncLoggerContextSelector;
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
+import org.apache.logging.log4j.core.util.Constants;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@Category(AsyncLoggers.class)
+public class AsyncLoggerThreadsTest {
+
+    @BeforeClass
+    public static void beforeClass() {
+        System.setProperty(Constants.LOG4J_CONTEXT_SELECTOR,
+                AsyncLoggerContextSelector.class.getName());
+        System.setProperty("java.util.logging.manager", org.apache.logging.log4j.jul.LogManager.class.getName());
+    }
+
+    @AfterClass
+    public static void afterClass() {
+        System.clearProperty(Constants.LOG4J_CONTEXT_SELECTOR);
+        System.clearProperty("java.util.logging.manager");
+    }
+
+    @Test
+    public void testAsyncLoggerThreads() {
+        LogManager.getLogger("com.foo.Bar").info("log");
+        List<Thread> asyncLoggerThreads = Thread.getAllStackTraces().keySet().stream()
+                .filter(thread -> thread.getName().matches("Log4j2-TF.*AsyncLogger.*"))
+                .collect(Collectors.toList());
+        assertEquals(asyncLoggerThreads.toString(), 1, asyncLoggerThreads.size());
+    }
+}
diff --git a/log4j-jul/src/test/java/org/apache/logging/log4j/jul/CoreLoggerTest.java b/log4j-jul/src/test/java/org/apache/logging/log4j/jul/CoreLoggerTest.java
index fc9b20f..ae91022 100644
--- a/log4j-jul/src/test/java/org/apache/logging/log4j/jul/CoreLoggerTest.java
+++ b/log4j-jul/src/test/java/org/apache/logging/log4j/jul/CoreLoggerTest.java
@@ -48,6 +48,7 @@
     @Before
     public void setUp() throws Exception {
         logger = Logger.getLogger(LOGGER_NAME);
+        logger.setFilter(null);
         assertThat(logger.getLevel(), equalTo(Level.FINE));
         eventAppender = ListAppender.getListAppender("TestAppender");
         flowAppender = ListAppender.getListAppender("FlowAppender");
diff --git a/log4j-kubernetes/src/main/java/org/apache/logging/log4j/kubernetes/ContainerUtil.java b/log4j-kubernetes/src/main/java/org/apache/logging/log4j/kubernetes/ContainerUtil.java
new file mode 100644
index 0000000..7944e86
--- /dev/null
+++ b/log4j-kubernetes/src/main/java/org/apache/logging/log4j/kubernetes/ContainerUtil.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.kubernetes;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.status.StatusLogger;
+
+/**
+ * Locate the current docker container.
+ */
+public class ContainerUtil {
+    private static final Logger LOGGER = StatusLogger.getLogger();
+    private static final int MAXLENGTH = 65;
+
+/**
+ * Returns the container id when running in a Docker container.
+ *
+ * This inspects /proc/self/cgroup looking for a Kubernetes Control Group. Once it finds one it attempts
+ * to isolate just the docker container id. There doesn't appear to be a standard way to do this, but
+ * it seems to be the only way to determine what the current container is in a multi-container pod. It would have
+ * been much nicer if Kubernetes would just put the container id in a standard environment variable.
+ *
+ * @see <a href="http://stackoverflow.com/a/25729598/12916">Stackoverflow</a> for a discussion on retrieving the containerId.
+ * @see <a href="https://github.com/jenkinsci/docker-workflow-plugin/blob/master/src/main/java/org/jenkinsci/plugins/docker/workflow/client/ControlGroup.java">ControlGroup</a>
+ * for the original version of this. Not much is actually left but it provided good inspiration.
+ * @return The container id.
+ */
+    public static String getContainerId() {
+        try {
+            File file = new File("/proc/self/cgroup");
+            if (file.exists()) {
+                Path path = file.toPath();
+                String id = Files.lines(path).map(ContainerUtil::getContainerId).filter(Objects::nonNull)
+                        .findFirst().orElse(null);
+                LOGGER.debug("Found container id {}", id);
+                return id;
+            } else {
+                LOGGER.warn("Unable to access container information");
+            }
+        } catch (IOException ioe) {
+            LOGGER.warn("Error obtaining container id: {}", ioe.getMessage());
+        }
+        return null;
+    }
+
+    private static String getContainerId(String line) {
+        // Every control group in Kubernetes will use
+        if (line.contains("/kubepods")) {
+            // Strip off everything up to the last slash.
+            int i = line.lastIndexOf('/');
+            if (i < 0) {
+                return null;
+            }
+            // If the remainder has a period then take everything up to it.
+            line = line.substring(i + 1);
+            i = line.lastIndexOf('.');
+            if (i > 0) {
+                line = line.substring(0, i);
+            }
+            // Everything ending with a '/' has already been stripped but the remainder might start with "docker-"
+            if (line.contains("docker-")) {
+                // 8:cpuset:/kubepods.slice/kubepods-pod9c26dfb6_b9c9_11e7_bfb9_02c6c1fc4861.slice/docker-3dd988081e7149463c043b5d9c57d7309e079c5e9290f91feba1cc45a04d6a5b.scope
+                i = line.lastIndexOf("docker-");
+                line = line.substring(i + 7);
+            }
+            return line.length() <= MAXLENGTH ? line : line.substring(0, MAXLENGTH);
+        }
+
+        return null;
+    }
+}
diff --git a/log4j-kubernetes/src/main/java/org/apache/logging/log4j/kubernetes/KubernetesClientBuilder.java b/log4j-kubernetes/src/main/java/org/apache/logging/log4j/kubernetes/KubernetesClientBuilder.java
index 414f9e7..79c942d 100644
--- a/log4j-kubernetes/src/main/java/org/apache/logging/log4j/kubernetes/KubernetesClientBuilder.java
+++ b/log4j-kubernetes/src/main/java/org/apache/logging/log4j/kubernetes/KubernetesClientBuilder.java
@@ -32,11 +32,19 @@
     private static final Logger LOGGER = StatusLogger.getLogger();
 
     public KubernetesClient createClient() {
-        return new DefaultKubernetesClient(kubernetesClientConfig());
+        Config config = kubernetesClientConfig();
+        return config != null ? new DefaultKubernetesClient(config) : null;
     }
 
     private Config kubernetesClientConfig() {
-        Config base = Config.autoConfigure(null);
+        Config base = null;
+        try {
+            base = Config.autoConfigure(null);
+        } catch (Exception ex) {
+            if (ex instanceof  NullPointerException) {
+                return null;
+            }
+        }
         KubernetesClientProperties props = new KubernetesClientProperties(base);
         Config properties = new ConfigBuilder(base)
                 .withApiVersion(props.getApiVersion())
diff --git a/log4j-kubernetes/src/main/java/org/apache/logging/log4j/kubernetes/KubernetesLookup.java b/log4j-kubernetes/src/main/java/org/apache/logging/log4j/kubernetes/KubernetesLookup.java
index 2dc770b..2a16a9e 100644
--- a/log4j-kubernetes/src/main/java/org/apache/logging/log4j/kubernetes/KubernetesLookup.java
+++ b/log4j-kubernetes/src/main/java/org/apache/logging/log4j/kubernetes/KubernetesLookup.java
@@ -18,6 +18,7 @@
 
 import java.net.URL;
 import java.nio.file.Paths;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
@@ -57,50 +58,99 @@
     private static final Lock initLock = new ReentrantLock();
     private static final boolean isSpringIncluded =
             LoaderUtil.isClassAvailable("org.apache.logging.log4j.spring.cloud.config.client.SpringEnvironmentHolder");
+    private Pod pod;
+    private Namespace namespace;
+    private URL masterUrl;
 
+    public KubernetesLookup() {
+        this.pod = null;
+        this.namespace = null;
+        this.masterUrl = null;
+        initialize();
+    }
+
+    KubernetesLookup(Pod pod, Namespace namespace, URL masterUrl) {
+        this.pod = pod;
+        this.namespace = namespace;
+        this.masterUrl = masterUrl;
+        initialize();
+    }
     private boolean initialize() {
         if (kubernetesInfo == null || (isSpringIncluded && !kubernetesInfo.isSpringActive)) {
             initLock.lock();
             boolean isSpringActive = isSpringActive();
             if (kubernetesInfo == null || (!kubernetesInfo.isSpringActive && isSpringActive)) {
                 try {
-                    KubernetesClient client = new KubernetesClientBuilder().createClient();
-                    if (client != null) {
-                        KubernetesInfo info = new KubernetesInfo();
-                        info.isSpringActive = isSpringActive;
-                        info.hostName = getHostname();
-                        Pod pod = getCurrentPod(info.hostName, client);
-                        if (pod != null) {
-                            info.app = pod.getMetadata().getLabels().get("app");
-                            final String app = info.app != null ? info.app : "";
-                            info.podTemplateHash = pod.getMetadata().getLabels().get("pod-template-hash");
-                            info.accountName = pod.getSpec().getServiceAccountName();
-                            info.clusterName = pod.getMetadata().getClusterName();
-                            info.hostIp = pod.getStatus().getHostIP();
-                            info.labels = pod.getMetadata().getLabels();
-                            info.podId = pod.getMetadata().getUid();
-                            info.podIp = pod.getStatus().getPodIP();
-                            info.podName = pod.getMetadata().getName();
-                            Container container = pod.getSpec().getContainers().stream()
-                                    .filter(c -> c.getName().equals(app)).findFirst().orElse(null);
-                            if (container != null) {
-                                info.containerName = container.getName();
-                                info.imageName = container.getImage();
-                            }
+                    KubernetesInfo info = new KubernetesInfo();
+                    KubernetesClient client = null;
+                    info.isSpringActive = isSpringActive;
+                    if (pod == null) {
+                        client = new KubernetesClientBuilder().createClient();
+                        if (client != null) {
+                            pod = getCurrentPod(System.getenv(HOSTNAME), client);
                             info.masterUrl = client.getMasterUrl();
-                            info.namespace = pod.getMetadata().getNamespace();
-                            Namespace namespace = client.namespaces().withName(info.namespace).get();
-                            if (namespace != null) {
-                                info.namespaceId = namespace.getMetadata().getUid();
+                            if (pod != null) {
+                                info.namespace = pod.getMetadata().getNamespace();
+                                namespace = namespace = client.namespaces().withName(info.namespace).get();
                             }
-                            ContainerStatus containerStatus = pod.getStatus().getContainerStatuses().stream()
-                                    .filter(cs -> cs.getName().equals(app)).findFirst().orElse(null);
-                            if (containerStatus != null) {
-                                info.containerId = containerStatus.getContainerID();
-                                info.imageId = containerStatus.getImageID();
-                            }
-                            kubernetesInfo = info;
+                        } else {
+                            LOGGER.warn("Kubernetes is not available for access");
                         }
+                    } else {
+                        info.masterUrl = masterUrl;
+                    }
+                    if (pod != null) {
+                        if (namespace != null) {
+                            info.namespaceId = namespace.getMetadata().getUid();
+                            info.namespaceAnnotations = namespace.getMetadata().getAnnotations();
+                            info.namespaceLabels = namespace.getMetadata().getLabels();
+                        }
+                        info.app = pod.getMetadata().getLabels().get("app");
+                        info.hostName = pod.getSpec().getNodeName();
+                        info.annotations = pod.getMetadata().getAnnotations();
+                        final String app = info.app != null ? info.app : "";
+                        info.podTemplateHash = pod.getMetadata().getLabels().get("pod-template-hash");
+                        info.accountName = pod.getSpec().getServiceAccountName();
+                        info.clusterName = pod.getMetadata().getClusterName();
+                        info.hostIp = pod.getStatus().getHostIP();
+                        info.labels = pod.getMetadata().getLabels();
+                        info.podId = pod.getMetadata().getUid();
+                        info.podIp = pod.getStatus().getPodIP();
+                        info.podName = pod.getMetadata().getName();
+                        ContainerStatus containerStatus = null;
+                        List<ContainerStatus> statuses = pod.getStatus().getContainerStatuses();
+                        if (statuses.size() == 1) {
+                            containerStatus = statuses.get(0);
+                        } else if (statuses.size() > 1) {
+                            String containerId = ContainerUtil.getContainerId();
+                            if (containerId != null) {
+                                containerStatus = statuses.stream()
+                                        .filter(cs -> cs.getContainerID().contains(containerId))
+                                        .findFirst().orElse(null);
+                            }
+                        }
+                        final String containerName;
+                        if (containerStatus != null) {
+                            info.containerId = containerStatus.getContainerID();
+                            info.imageId = containerStatus.getImageID();
+                            containerName = containerStatus.getName();
+                        } else {
+                            containerName = null;
+                        }
+                        Container container = null;
+                        List<Container> containers = pod.getSpec().getContainers();
+                        if (containers.size() == 1) {
+                            container = containers.get(0);
+                        } else if (containers.size() > 1 && containerName != null) {
+                            container = containers.stream().filter(c -> c.getName().equals(containerName))
+                                    .findFirst().orElse(null);
+                        }
+                        if (container != null) {
+                            info.containerName = container.getName();
+                            info.imageName = container.getImage();
+                        }
+
+                        kubernetesInfo = info;
                     }
                 } finally {
                     initLock.unlock();
@@ -112,13 +162,16 @@
 
     @Override
     public String lookup(LogEvent event, String key) {
-        if (!initialize()) {
+        if (kubernetesInfo == null) {
             return null;
         }
         switch (key) {
             case "accountName": {
                 return kubernetesInfo.accountName;
             }
+            case "annotations": {
+                return kubernetesInfo.annotations.toString();
+            }
             case "containerId": {
                 return kubernetesInfo.containerId;
             }
@@ -146,9 +199,15 @@
             case "masterUrl": {
                 return kubernetesInfo.masterUrl.toString();
             }
+            case "namespaceAnnotations": {
+                return kubernetesInfo.namespaceAnnotations.toString();
+            }
             case "namespaceId": {
                 return kubernetesInfo.namespaceId;
             }
+            case "namespaceLabels": {
+                return kubernetesInfo.namespaceLabels.toString();
+            }
             case "namespaceName": {
                 return kubernetesInfo.namespace;
             }
@@ -172,6 +231,13 @@
         }
     }
 
+    /**
+     * For unit testing only.
+     */
+    void clearInfo() {
+        kubernetesInfo = null;
+    }
+
     private String getHostname() {
         return System.getenv(HOSTNAME);
     }
@@ -201,6 +267,7 @@
     private static class KubernetesInfo {
         boolean isSpringActive;
         String accountName;
+        Map<String, String> annotations;
         String app;
         String clusterName;
         String containerId;
@@ -212,7 +279,9 @@
         Map<String, String> labels;
         URL masterUrl;
         String namespace;
+        Map<String, String> namespaceAnnotations;
         String namespaceId;
+        Map<String, String> namespaceLabels;
         String podId;
         String podIp;
         String podName;
diff --git a/log4j-kubernetes/src/test/java/org/apache/logging/log4j/kubernetes/KubernetesLookupTest.java b/log4j-kubernetes/src/test/java/org/apache/logging/log4j/kubernetes/KubernetesLookupTest.java
new file mode 100644
index 0000000..7174596
--- /dev/null
+++ b/log4j-kubernetes/src/test/java/org/apache/logging/log4j/kubernetes/KubernetesLookupTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.kubernetes;
+
+import java.io.File;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import io.fabric8.kubernetes.api.model.Namespace;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.Pod;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Validate the Kubernetes Lookup.
+ */
+public class KubernetesLookupTest {
+
+    private static final String localJson = "target/test-classes/localPod.json";
+    private static final String clusterJson = "target/test-classes/clusterPod.json";
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+    public static URL masterUrl;
+
+    @BeforeClass
+    public static void beforeClass() throws Exception {
+        masterUrl = new URL("http://localhost:443/");
+    }
+
+    @Test
+    public void testLocal() throws Exception {
+        Pod pod = objectMapper.readValue(new File(localJson), Pod.class);
+        Namespace namespace = createNamespace();
+        KubernetesLookup lookup = new KubernetesLookup(pod, namespace, masterUrl);
+        try {
+            assertEquals("Incorrect container name", "sampleapp", lookup.lookup("containerName"));
+            assertEquals("Incorrect container id",
+                    "docker://818b0098946c67e6ac56cb7c0934b7c2a9f50feb7244b422b2a7f566f7e5d0df",
+                    lookup.lookup("containerId"));
+            assertEquals("Incorrect host name", "docker-desktop", lookup.lookup("host"));
+            assertEquals("Incorrect pod name", "sampleapp-584f99476d-mnrp4", lookup.lookup("podName"));
+        } finally {
+            lookup.clearInfo();;
+        }
+    }
+
+    @Test
+    public void testCluster() throws Exception {
+        Pod pod = objectMapper.readValue(new File(clusterJson), Pod.class);
+        Namespace namespace = createNamespace();
+        KubernetesLookup lookup = new KubernetesLookup(pod, namespace, masterUrl);
+        try {
+            assertEquals("Incorrect container name", "platform-forms-service", lookup.lookup("containerName"));
+            assertEquals("Incorrect container id",
+                    "docker://2b7c2a93dfb48334aa549e29fdd38039ddd256eec43ba64c145fa4b75a1542f0",
+                    lookup.lookup("containerId"));
+            assertEquals("Incorrect host name", "k8s-tmpcrm-worker-s03-04", lookup.lookup("host"));
+            assertEquals("Incorrect pod name", "platform-forms-service-primary-5ddfc4f9b8-kfpzv", lookup.lookup("podName"));
+        } finally {
+            lookup.clearInfo();
+        }
+    }
+
+    private Namespace createNamespace() {
+        Namespace namespace = new Namespace();
+        ObjectMeta meta = new ObjectMeta();
+        Map<String, String> annotations = new HashMap<>();
+        annotations.put("test", "name");
+        meta.setAnnotations(annotations);
+        Map<String, String> labels = new HashMap<>();
+        labels.put("ns", "my-namespace");
+        meta.setLabels(labels);
+        meta.setUid(UUID.randomUUID().toString());
+        namespace.setMetadata(meta);
+        return namespace;
+    }
+}
diff --git a/log4j-kubernetes/src/test/resources/clusterPod.json b/log4j-kubernetes/src/test/resources/clusterPod.json
new file mode 100644
index 0000000..7bae9c3
--- /dev/null
+++ b/log4j-kubernetes/src/test/resources/clusterPod.json
@@ -0,0 +1,177 @@
+{
+  "apiVersion": "v1",
+  "kind": "Pod",
+  "metadata": {
+    "annotations": {
+      "cni.projectcalico.org/podIP": "172.16.55.101/32",
+      "cni.projectcalico.org/podIPs": "172.16.55.101/32",
+      "flagger-id": "94d53b7b-cc06-41b3-bbac-a2d14a16d95d",
+      "prometheus.io/port": "9797",
+      "prometheus.io/scrape": "true"
+    },
+    "creationTimestamp": "2020-06-15T15:44:16Z",
+    "generateName": "platform-forms-service-primary-5ddfc4f9b8-",
+    "labels": {
+      "app": "platform-forms-service-primary",
+      "pod-template-hash": "5ddfc4f9b8"
+    },
+    "name": "platform-forms-service-primary-5ddfc4f9b8-kfpzv",
+    "namespace": "default",
+    "ownerReferences": [
+      {
+        "apiVersion": "apps/v1",
+        "kind": "ReplicaSet",
+        "blockOwnerDeletion": true,
+        "controller": true,
+        "name": "platform-forms-service-primary-5ddfc4f9b8",
+        "uid": "d2e89c56-7623-439e-a9ee-4a67e2f3a81a"
+      }],
+    "resourceVersion": "37382150",
+    "selfLink": "/api/v1/namespaces/default/pods/platform-forms-service-primary-5ddfc4f9b8-kfpzv",
+    "uid": "df8cbac1-129c-4cd3-b5bc-65d72d8ba5f0"
+  },
+  "spec": {
+    "containers": [
+      {
+        "env": [
+          {
+            "name": "APACHE_ENV",
+            "value": "tmpcrm"
+          },
+          {
+            "name": "SPRING_PROFILES_ACTIVE",
+            "value": "tmpcrm"
+          },
+          {
+            "name": "JAVA_OPTS",
+            "value": "-Dlogging.label=crm"
+          }],
+        "image": "docker.apache.xyz/platform-forms-service:0.15.0",
+        "imagePullPolicy": "Always",
+        "livenessProbe": {
+          "failureThreshold": 3,
+          "httpGet": {
+            "path": "/info",
+            "port": "http",
+            "scheme": "HTTP"
+          },
+          "periodSeconds": 10,
+          "successThreshold": 1,
+          "timeoutSeconds": 1
+        },
+        "name": "platform-forms-service",
+        "ports": [
+          {
+            "containerPort": 8080,
+            "name": "http",
+            "protocol": "TCP"
+          }],
+        "readinessProbe": {
+          "failureThreshold": 3,
+          "httpGet": {
+            "path": "/health",
+            "port": "http",
+            "scheme": "HTTP"
+          },
+          "periodSeconds": 10,
+          "successThreshold": 1,
+          "timeoutSeconds": 1
+        },
+        "resources": {
+        },
+        "securityContext": {
+        },
+        "terminationMessagePath": "/dev/termination-log",
+        "terminationMessagePolicy": "File",
+        "volumeMounts": [
+          {
+            "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
+            "name": "default-token-2nqlw",
+            "readOnly": true
+          }]
+      }],
+    "dnsPolicy": "ClusterFirst",
+    "enableServiceLinks": true,
+    "nodeName": "k8s-tmpcrm-worker-s03-04",
+    "priority": 0,
+    "restartPolicy": "Always",
+    "schedulerName": "default-scheduler",
+    "securityContext": {
+    },
+    "serviceAccount": "default",
+    "serviceAccountName": "default",
+    "terminationGracePeriodSeconds": 30,
+    "tolerations": [
+      {
+        "effect": "NoExecute",
+        "key": "node.kubernetes.io/not-ready",
+        "operator": "Exists",
+        "tolerationSeconds": 300
+      },
+      {
+        "effect": "NoExecute",
+        "key": "node.kubernetes.io/unreachable",
+        "operator": "Exists",
+        "tolerationSeconds": 300
+      }],
+    "volumes": [
+      {
+        "name": "default-token-2nqlw",
+        "secret": {
+          "defaultMode": 420,
+          "secretName": "default-token-2nqlw"
+        }
+      }]
+  },
+  "status": {
+    "conditions": [
+      {
+        "lastTransitionTime": "2020-06-15T15:44:16Z",
+        "status": "True",
+        "type": "Initialized"
+      },
+      {
+        "lastTransitionTime": "2020-06-15T15:44:46Z",
+        "status": "True",
+        "type": "Ready"
+      },
+      {
+        "lastTransitionTime": "2020-06-15T15:44:46Z",
+        "status": "True",
+        "type": "ContainersReady"
+      },
+      {
+        "lastTransitionTime": "2020-06-15T15:44:16Z",
+        "status": "True",
+        "type": "PodScheduled"
+      }],
+    "containerStatuses": [
+      {
+        "containerID": "docker://2b7c2a93dfb48334aa549e29fdd38039ddd256eec43ba64c145fa4b75a1542f0",
+        "image": "docker.apache.xyz/platform-forms-service:0.15.0",
+        "imageID":
+        "docker-pullable://docker.apache.xyz/platform-forms-service@sha256:45fd19ccd99e218a7685c4cee5bc5b16aeae1cdb8e8773f9c066d4cfb22ee195",
+        "lastState": {
+        },
+        "name": "platform-forms-service",
+        "ready": true,
+        "restartCount": 0,
+        "state": {
+          "running": {
+            "startedAt": "2020-06-15T15:44:21Z"
+          }
+        },
+        "started": true
+      }],
+    "hostIP": "10.103.220.170",
+    "phase": "Running",
+    "podIP": "172.16.55.101",
+    "qosClass": "BestEffort",
+    "startTime": "2020-06-15T15:44:16Z",
+    "podIPs": [
+      {
+        "ip": "172.16.55.101"
+      }]
+  }
+}
+
diff --git a/log4j-kubernetes/src/test/resources/localPod.json b/log4j-kubernetes/src/test/resources/localPod.json
new file mode 100644
index 0000000..3aeef46
--- /dev/null
+++ b/log4j-kubernetes/src/test/resources/localPod.json
@@ -0,0 +1,141 @@
+{
+  "apiVersion": "v1",
+  "kind": "Pod",
+  "metadata": {
+    "creationTimestamp": "2020-06-14T21:50:09Z",
+    "generateName": "sampleapp-584f99476d-",
+    "labels": {
+      "app": "sampleapp",
+      "pod-template-hash": "584f99476d"
+    },
+    "name": "sampleapp-584f99476d-mnrp4",
+    "namespace": "default",
+    "ownerReferences": [
+      {
+        "apiVersion": "apps/v1",
+        "kind": "ReplicaSet",
+        "blockOwnerDeletion": true,
+        "controller": true,
+        "name": "sampleapp-584f99476d",
+        "uid": "d68146d1-17c4-486e-aa8d-07d7d5d38b94"
+      }],
+    "resourceVersion": "1200430",
+    "selfLink": "/api/v1/namespaces/default/pods/sampleapp-584f99476d-mnrp4",
+    "uid": "9213879a-479c-42ce-856b-7e2666d21829"
+  },
+  "spec": {
+    "containers": [
+      {
+        "env": [
+          {
+            "name": "JAVA_OPTS",
+            "value": "-Delastic.search.host=host.docker.internal"
+          }],
+        "image": "localhost:5000/sampleapp:latest",
+        "imagePullPolicy": "Always",
+        "name": "sampleapp",
+        "ports": [
+          {
+            "containerPort": 8080,
+            "protocol": "TCP"
+          },
+          {
+            "containerPort": 5005,
+            "protocol": "TCP"
+          }],
+        "resources": {
+        },
+        "terminationMessagePath": "/dev/termination-log",
+        "terminationMessagePolicy": "File",
+        "volumeMounts": [
+          {
+            "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
+            "name": "default-token-jzq7d",
+            "readOnly": true
+          }]
+      }],
+    "dnsPolicy": "ClusterFirst",
+    "nodeName": "docker-desktop",
+    "priority": 0,
+    "restartPolicy": "Always",
+    "schedulerName": "default-scheduler",
+    "securityContext": {
+    },
+    "serviceAccount": "default",
+    "serviceAccountName": "default",
+    "terminationGracePeriodSeconds": 30,
+    "tolerations": [
+      {
+        "effect": "NoExecute",
+        "key": "node.kubernetes.io/not-ready",
+        "operator": "Exists",
+        "tolerationSeconds": 300
+      },
+      {
+        "effect": "NoExecute",
+        "key": "node.kubernetes.io/unreachable",
+        "operator": "Exists",
+        "tolerationSeconds": 300
+      }],
+    "volumes": [
+      {
+        "name": "default-token-jzq7d",
+        "secret": {
+          "defaultMode": 420,
+          "secretName": "default-token-jzq7d"
+        }
+      }],
+    "enableServiceLinks": true
+  },
+  "status": {
+    "conditions": [
+      {
+        "lastTransitionTime": "2020-06-14T21:50:09Z",
+        "status": "True",
+        "type": "Initialized"
+      },
+      {
+        "lastTransitionTime": "2020-06-14T21:50:10Z",
+        "status": "True",
+        "type": "Ready"
+      },
+      {
+        "lastTransitionTime": "2020-06-14T21:50:10Z",
+        "status": "True",
+        "type": "ContainersReady"
+      },
+      {
+        "lastTransitionTime": "2020-06-14T21:50:09Z",
+        "status": "True",
+        "type": "PodScheduled"
+      }],
+    "containerStatuses": [
+      {
+        "containerID": "docker://818b0098946c67e6ac56cb7c0934b7c2a9f50feb7244b422b2a7f566f7e5d0df",
+        "image": "sampleapp:latest",
+        "imageID":
+        "docker-pullable://localhost:5000/sampleapp@sha256:3cefb2db514db73c69854fee8abd072f27240519432d08aad177a57ee34b7d39",
+        "lastState": {
+        },
+        "name": "sampleapp",
+        "ready": true,
+        "restartCount": 0,
+        "state": {
+          "running": {
+            "startedAt": "2020-06-14T21:50:10Z"
+          }
+        },
+        "started": true
+      }],
+    "hostIP": "192.168.65.3",
+    "phase": "Running",
+    "podIP": "10.1.0.47",
+    "qosClass": "BestEffort",
+    "startTime": "2020-06-14T21:50:09Z",
+    "podIPs": [
+      {
+        "ip": "10.1.0.47"
+      }]
+  }
+}
+
diff --git a/log4j-layout-jackson-json/src/test/java/org/apache/logging/log4j/jackson/json/layout/JsonLayoutTest.java b/log4j-layout-jackson-json/src/test/java/org/apache/logging/log4j/jackson/json/layout/JsonLayoutTest.java
index 1c22f4d..2b08e13 100644
--- a/log4j-layout-jackson-json/src/test/java/org/apache/logging/log4j/jackson/json/layout/JsonLayoutTest.java
+++ b/log4j-layout-jackson-json/src/test/java/org/apache/logging/log4j/jackson/json/layout/JsonLayoutTest.java
@@ -632,4 +632,18 @@
 	private String toPropertySeparator(final boolean compact) {
         return compact ? ":" : " : ";
     }
+
+    @Test   // LOG4J2-2749 (#362)
+    public void testEmptyValuesAreIgnored() {
+        final AbstractJacksonLayout layout = JsonLayout
+                .newBuilder()
+                .setAdditionalFields(new KeyValuePair[] {
+                        new KeyValuePair("empty", "${ctx:empty:-}")
+                })
+                .setConfiguration(ctx.getConfiguration())
+                .build();
+        final String str = layout.toSerializable(LogEventFixtures.createLogEvent());
+        assertFalse(str, str.contains("\"empty\""));
+    }
+
 }
diff --git a/log4j-layout-json-template/pom.xml b/log4j-layout-json-template/pom.xml
new file mode 100644
index 0000000..f9e74d6
--- /dev/null
+++ b/log4j-layout-json-template/pom.xml
@@ -0,0 +1,552 @@
+<?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>3.0.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.apache.logging.log4j</groupId>
+      <artifactId>log4j-layout-jackson-json</artifactId>
+      <version>${project.version}</version>
+      <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>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>
+        <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/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..355816e
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java
@@ -0,0 +1,696 @@
+/*
+ * 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.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.KeyValuePair;
+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.plugins.Node;
+import org.apache.logging.log4j.plugins.Plugin;
+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);
+            if (eventDelimiter != null && eventDelimiter.equalsIgnoreCase("null")) {
+                stringBuilder.append('\0');
+            } else {
+                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> {
+
+            @org.apache.logging.log4j.plugins.PluginBuilderAttribute
+            private String key;
+
+            @org.apache.logging.log4j.plugins.PluginBuilderAttribute
+            private String value;
+
+            @org.apache.logging.log4j.plugins.PluginBuilderAttribute
+            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-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolver.java
similarity index 81%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
copy to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolver.java
index 3e7e0f3..ce21181 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolver.java
@@ -14,11 +14,8 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
+package org.apache.logging.log4j.layout.json.template.resolver;
 
-package org.apache.logging.log4j.mongodb3;
+import org.apache.logging.log4j.core.LogEvent;
 
-public class TestConstants {
-
-    public static final String SYS_PROP_NAME_PORT = "MongoDBTestPort";
-
-}
+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-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactory.java
similarity index 75%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
copy to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactory.java
index 3e7e0f3..3c2f2db 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverFactory.java
@@ -14,11 +14,8 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
+package org.apache.logging.log4j.layout.json.template.resolver;
 
-package org.apache.logging.log4j.mongodb3;
+import org.apache.logging.log4j.core.LogEvent;
 
-public class TestConstants {
-
-    public static final String SYS_PROP_NAME_PORT = "MongoDBTestPort";
-
-}
+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..492d447
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java
@@ -0,0 +1,260 @@
+/*
+ * 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.layout.PatternLayout;
+import org.apache.logging.log4j.core.util.JsonUtils;
+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" };
+    /**
+     * Default length for new StringBuilder instances: {@value} .
+     */
+    protected static final int DEFAULT_STRING_BUILDER_SIZE = 1024;
+
+    private final EventResolver internalResolver;
+
+    private PatternLayout patternLayout;
+
+    MessageResolver(final Configuration configuration, final TemplateResolverConfig config) {
+        this.internalResolver = createInternalResolver(configuration, config);
+    }
+
+    static String getName() {
+        return "message";
+    }
+
+    private EventResolver createInternalResolver(final Configuration configuration,
+            final TemplateResolverConfig config) {
+        final boolean stringified = config.getBoolean("stringified", false);
+        final String fallbackKey = config.getString("fallbackKey");
+        final String pattern = config.getString("pattern");
+        final boolean includeStacktrace = config.getBoolean("includeStacktrace", true);
+        if (pattern != null) {
+            patternLayout = PatternLayout.newBuilder().setPattern(pattern)
+                    .setAlwaysWriteExceptions(includeStacktrace)
+                    .setConfiguration(configuration)
+                    .build();
+        } else {
+            patternLayout = null;
+        }
+        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 EventResolver createStringResolver(final String fallbackKey) {
+        return (final LogEvent logEvent, final JsonWriter jsonWriter) ->
+                resolveString(fallbackKey, logEvent, jsonWriter);
+    }
+
+    private void resolveString(
+            final String fallbackKey,
+            final LogEvent logEvent,
+            final JsonWriter jsonWriter) {
+        if (patternLayout != null) {
+            final StringBuilder messageBuffer = getMessageStringBuilder();
+            patternLayout.serialize(logEvent, messageBuffer);
+            jsonWriter.writeString(messageBuffer.toString());
+        } else {
+            final Message message = logEvent.getMessage();
+            resolveString(fallbackKey, message, jsonWriter);
+        }
+    }
+
+    private 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 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 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 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;
+
+    }
+
+    private static final ThreadLocal<StringBuilder> messageStringBuilder = new ThreadLocal<>();
+
+    private static StringBuilder getMessageStringBuilder() {
+        StringBuilder result = messageStringBuilder.get();
+        if (result == null) {
+            result = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
+            messageStringBuilder.set(result);
+        }
+        result.setLength(0);
+        return result;
+    }
+
+}
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..59e07f9
--- /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(context.getConfiguration(), 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..d5029e9
--- /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()
+                .setConfiguration(context.getConfiguration())
+                .setCharset(context.getCharset())
+                .setPattern(pattern)
+                .setAlwaysWriteExceptions(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-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/package-info.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceResolver.java
similarity index 84%
copy from log4j-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/package-info.java
copy to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceResolver.java
index bf111af..8275193 100644
--- a/log4j-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/package-info.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceResolver.java
@@ -14,7 +14,6 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
-/**
- * The classes in this package contain the MongoDB provider for the NoSQL Appender.
- */
-package org.apache.logging.log4j.mongodb2;
+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-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverConfig.java
similarity index 72%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
copy to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverConfig.java
index 3e7e0f3..a83fffa 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverConfig.java
@@ -14,11 +14,16 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
+package org.apache.logging.log4j.layout.json.template.resolver;
 
-package org.apache.logging.log4j.mongodb3;
+import org.apache.logging.log4j.layout.json.template.util.MapAccessor;
 
-public class TestConstants {
+import java.util.Map;
 
-    public static final String SYS_PROP_NAME_PORT = "MongoDBTestPort";
+class TemplateResolverConfig extends MapAccessor {
+
+    TemplateResolverConfig(final Map<String, Object> map) {
+        super(map);
+    }
 
 }
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverContext.java
similarity index 60%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
copy to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverContext.java
index 3e7e0f3..74687d2 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverContext.java
@@ -14,11 +14,21 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
+package org.apache.logging.log4j.layout.json.template.resolver;
 
-package org.apache.logging.log4j.mongodb3;
+import org.apache.logging.log4j.core.lookup.StrSubstitutor;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
 
-public class TestConstants {
+import java.util.Map;
 
-    public static final String SYS_PROP_NAME_PORT = "MongoDBTestPort";
+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-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
similarity index 75%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
copy to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
index 3e7e0f3..3e3c8ef 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolverFactory.java
@@ -14,11 +14,12 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
+package org.apache.logging.log4j.layout.json.template.resolver;
 
-package org.apache.logging.log4j.mongodb3;
+interface TemplateResolverFactory<V, C extends TemplateResolverContext<V, C>, R extends TemplateResolver<V>> {
 
-public class TestConstants {
+    String getName();
 
-    public static final String SYS_PROP_NAME_PORT = "MongoDBTestPort";
+    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..3d023b3
--- /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.time.internal.format.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-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecycler.java
similarity index 65%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
copy to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecycler.java
index 3e7e0f3..2aae11f 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/DummyRecycler.java
@@ -14,11 +14,24 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
+package org.apache.logging.log4j.layout.json.template.util;
 
-package org.apache.logging.log4j.mongodb3;
+import java.util.function.Supplier;
 
-public class TestConstants {
+public class DummyRecycler<V> implements Recycler<V> {
 
-    public static final String SYS_PROP_NAME_PORT = "MongoDBTestPort";
+    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-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Recycler.java
similarity index 84%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
copy to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Recycler.java
index 3e7e0f3..b6a0c89 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Recycler.java
@@ -14,11 +14,12 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
+package org.apache.logging.log4j.layout.json.template.util;
 
-package org.apache.logging.log4j.mongodb3;
+public interface Recycler<V> {
 
-public class TestConstants {
+    V acquire();
 
-    public static final String SYS_PROP_NAME_PORT = "MongoDBTestPort";
+    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..a6937c6
--- /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.util.Constants;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.convert.TypeConverter;
+import org.apache.logging.log4j.plugins.convert.TypeConverters;
+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-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactory.java
similarity index 67%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
copy to log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactory.java
index 3e7e0f3..3b7737c 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactory.java
@@ -14,11 +14,18 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
+package org.apache.logging.log4j.layout.json.template.util;
 
-package org.apache.logging.log4j.mongodb3;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
 
-public class TestConstants {
+@FunctionalInterface
+public interface RecyclerFactory {
 
-    public static final String SYS_PROP_NAME_PORT = "MongoDBTestPort";
+    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-mongodb2/src/site/site.xml b/log4j-layout-json-template/src/site/site.xml
similarity index 97%
copy from log4j-mongodb2/src/site/site.xml
copy to log4j-layout-json-template/src/site/site.xml
index f5db26e..962392e 100644
--- a/log4j-mongodb2/src/site/site.xml
+++ b/log4j-layout-json-template/src/site/site.xml
@@ -13,13 +13,14 @@
  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 MongoDB 2.x Appender"
+<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/"/>
@@ -48,5 +49,7 @@
       <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-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java
similarity index 72%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
copy to log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java
index 3e7e0f3..a2ebe6f 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java
@@ -14,11 +14,16 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
+package org.apache.logging.log4j.layout.json.template;
 
-package org.apache.logging.log4j.mongodb3;
+import com.fasterxml.jackson.databind.ObjectMapper;
 
-public class TestConstants {
+public enum JacksonFixture {;
 
-    public static final String SYS_PROP_NAME_PORT = "MongoDBTestPort";
+    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..6996ea5
--- /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.jackson.json.layout.JsonLayout;
+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 JsonLayoutTest {
+
+    private static final Configuration CONFIGURATION = new DefaultConfiguration();
+
+    private static final JsonTemplateLayout JSON_TEMPLATE_LAYOUT = JsonTemplateLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .setEventTemplateUri("classpath:JsonLayout.json")
+            .build();
+
+    private static final JsonLayout JSON_LAYOUT = JsonLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .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> jsonLayoutMap = renderUsingJsonLayout(logEvent);
+        // JsonLayout blindly serializes the Throwable as a POJO, this is,
+        // to say the least, quite wrong, and I ain't gonna try to emulate
+        // this behaviour in JsonTemplateLayout. Hence, discarding the "thrown"
+        // field.
+        jsonTemplateLayoutMap.remove("thrown");
+        jsonLayoutMap.remove("thrown");
+        Assertions.assertThat(jsonTemplateLayoutMap).isEqualTo(jsonLayoutMap);
+    }
+
+    private static Map<String, Object> renderUsingJsonTemplateLayout(
+            final LogEvent logEvent) {
+        return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
+    }
+
+    private static Map<String, Object> renderUsingJsonLayout(
+            final LogEvent logEvent) {
+        return renderUsing(logEvent, JSON_LAYOUT);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutConcurrentEncodeTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutConcurrentEncodeTest.java
new file mode 100644
index 0000000..d17a8be
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutConcurrentEncodeTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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.LogEvent;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.layout.ByteBufferDestination;
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+public class JsonTemplateLayoutConcurrentEncodeTest {
+
+    private static class ConcurrentAccessError extends RuntimeException {
+
+        public static final long serialVersionUID = 0;
+
+        private ConcurrentAccessError(final int concurrentAccessCount) {
+            super("concurrentAccessCount=" + concurrentAccessCount);
+        }
+
+    }
+
+    private static class ConcurrentAccessDetectingByteBufferDestination
+            extends BlackHoleByteBufferDestination {
+
+        private final AtomicInteger concurrentAccessCounter = new AtomicInteger(0);
+
+        ConcurrentAccessDetectingByteBufferDestination() {
+            super(2_000);
+        }
+
+        @Override
+        public ByteBuffer getByteBuffer() {
+            final int concurrentAccessCount = concurrentAccessCounter.incrementAndGet();
+            if (concurrentAccessCount > 1) {
+                throw new ConcurrentAccessError(concurrentAccessCount);
+            }
+            try {
+                return super.getByteBuffer();
+            } finally {
+                concurrentAccessCounter.decrementAndGet();
+            }
+        }
+
+        @Override
+        public ByteBuffer drain(final ByteBuffer byteBuffer) {
+            final int concurrentAccessCount = concurrentAccessCounter.incrementAndGet();
+            if (concurrentAccessCount > 1) {
+                throw new ConcurrentAccessError(concurrentAccessCount);
+            }
+            try {
+                return super.drain(byteBuffer);
+            } finally {
+                concurrentAccessCounter.decrementAndGet();
+            }
+        }
+
+        @Override
+        public void writeBytes(final ByteBuffer byteBuffer) {
+            final int concurrentAccessCount = concurrentAccessCounter.incrementAndGet();
+            if (concurrentAccessCount > 1) {
+                throw new ConcurrentAccessError(concurrentAccessCount);
+            }
+            try {
+                super.writeBytes(byteBuffer);
+            } finally {
+                concurrentAccessCounter.decrementAndGet();
+            }
+        }
+
+        @Override
+        public void writeBytes(final byte[] buffer, final int offset, final int length) {
+            int concurrentAccessCount = concurrentAccessCounter.incrementAndGet();
+            if (concurrentAccessCount > 1) {
+                throw new ConcurrentAccessError(concurrentAccessCount);
+            }
+            try {
+                super.writeBytes(buffer, offset, length);
+            } finally {
+                concurrentAccessCounter.decrementAndGet();
+            }
+        }
+
+    }
+
+    private static final LogEvent[] LOG_EVENTS = createMessages();
+
+    private static LogEvent[] createMessages() {
+        final int messageCount = 1_000;
+        final LogEvent[] logEvents = new LogEvent[messageCount];
+        LogEventFixture
+                .createLiteLogEvents(messageCount)
+                .toArray(logEvents);
+        return logEvents;
+    }
+
+    @Test
+    public void test_concurrent_encode() {
+        final AtomicReference<Exception> encodeFailureRef = new AtomicReference<>(null);
+        produce(encodeFailureRef);
+        Assertions.assertThat(encodeFailureRef.get()).isNull();
+    }
+
+    private void produce(final AtomicReference<Exception> encodeFailureRef) {
+        final int threadCount = 10;
+        final JsonTemplateLayout layout = createLayout();
+        final ByteBufferDestination destination =
+                new ConcurrentAccessDetectingByteBufferDestination();
+        final AtomicLong encodeCounter = new AtomicLong(0);
+        final List<Thread> workers = IntStream
+                .range(0, threadCount)
+                .mapToObj((final int threadIndex) ->
+                        createWorker(
+                                layout,
+                                destination,
+                                encodeFailureRef,
+                                encodeCounter,
+                                threadIndex))
+                .collect(Collectors.toList());
+        workers.forEach(Thread::start);
+        workers.forEach((final Thread worker) -> {
+            try {
+                worker.join();
+            } catch (final InterruptedException ignored) {
+                System.err.format("join to %s interrupted%n", worker.getName());
+            }
+        });
+    }
+
+    private static JsonTemplateLayout createLayout() {
+        final Configuration config = new DefaultConfiguration();
+        return JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(config)
+                .setEventTemplate("{\"message\": \"${json:message}\"}")
+                .setStackTraceEnabled(false)
+                .setLocationInfoEnabled(false)
+                .build();
+    }
+
+    private Thread createWorker(
+            final JsonTemplateLayout layout,
+            final ByteBufferDestination destination,
+            final AtomicReference<Exception> encodeFailureRef,
+            final AtomicLong encodeCounter,
+            final int threadIndex) {
+        final int maxEncodeCount = 1_000;
+        final String threadName = String.format("Worker-%d", threadIndex);
+        return new Thread(
+                () -> {
+                    try {
+                        for (int logEventIndex = threadIndex % LOG_EVENTS.length;
+                             encodeFailureRef.get() == null && encodeCounter.incrementAndGet() < maxEncodeCount;
+                             logEventIndex = (logEventIndex + 1) % LOG_EVENTS.length) {
+                            final LogEvent logEvent = LOG_EVENTS[logEventIndex];
+                            layout.encode(logEvent, destination);
+                        }
+                    } catch (final Exception error) {
+                        final boolean succeeded = encodeFailureRef.compareAndSet(null, error);
+                        if (succeeded) {
+                            System.err.format("%s failed%n", threadName);
+                            error.printStackTrace(System.err);
+                        }
+                    }
+                },
+                threadName);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutGcFreeTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutGcFreeTest.java
new file mode 100644
index 0000000..dfe097a
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutGcFreeTest.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;
+
+import org.apache.logging.log4j.core.GcFreeLoggingTestUtil;
+import org.junit.Test;
+
+public class JsonTemplateLayoutGcFreeTest {
+
+    @Test
+    public void test_no_allocation_during_steady_state_logging() throws Exception {
+        GcFreeLoggingTestUtil.runTest(getClass());
+    }
+
+    /**
+     * This code runs in a separate process, instrumented with the Google Allocation Instrumenter.
+     */
+    public static void main(final String[] args) throws Exception {
+        System.setProperty("log4j.layout.jsonTemplate.recyclerFactory", "threadLocal");
+        System.setProperty("log4j2.garbagefree.threadContextMap", "true");
+        GcFreeLoggingTestUtil.executeLogging(
+                "gcFreeJsonTemplateLayoutLogging.xml",
+                JsonTemplateLayoutGcFreeTest.class);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutNullEventDelimiterTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutNullEventDelimiterTest.java
new file mode 100644
index 0000000..2487b65
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutNullEventDelimiterTest.java
@@ -0,0 +1,127 @@
+package org.apache.logging.log4j.layout.json.template;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.assertj.core.api.Assertions;
+import org.awaitility.Awaitility;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.time.Duration;
+
+public class JsonTemplateLayoutNullEventDelimiterTest {
+
+    // Set the configuration.
+    static {
+        System.setProperty(
+                "log4j.configurationFile",
+                "nullEventDelimitedJsonTemplateLayoutLogging.xml");
+    }
+
+    // Note that this port is hardcoded in the configuration file too!
+    private static final int PORT = 50514;
+
+    @Test
+    public void test() throws Exception {
+
+        // Set the expected bytes.
+        final byte[] expectedBytes = {
+                '"', 'f', 'o', 'o', '"', '\0',
+                '"', 'b', 'a', 'r', '"', '\0'
+        };
+
+        // Start the TCP server.
+        try (final TcpServer server = new TcpServer(PORT)) {
+
+            // Produce log events.
+            final Logger logger = LogManager.getLogger(JsonTemplateLayoutNullEventDelimiterTest.class);
+            logger.log(Level.INFO, "foo");
+            logger.log(Level.INFO, "bar");
+
+            // Wait for the log events.
+            Awaitility
+                    .await()
+                    .atMost(Duration.ofSeconds(10))
+                    .pollDelay(Duration.ofSeconds(2))
+                    .until(() -> server.getTotalReadByteCount() >= expectedBytes.length);
+
+            // Verify the received log events.
+            final byte[] actualBytes = server.getReceivedBytes();
+            Assertions.assertThat(actualBytes).startsWith(expectedBytes);
+
+        }
+
+    }
+
+    private static final class TcpServer extends Thread implements AutoCloseable {
+
+        private final ServerSocket serverSocket;
+
+        private final ByteArrayOutputStream outputStream;
+
+        private volatile int totalReadByteCount = 0;
+
+        private volatile boolean closed = false;
+
+        private TcpServer(final int port) throws IOException {
+            this.serverSocket = new ServerSocket(port);
+            this.outputStream = new ByteArrayOutputStream();
+            serverSocket.setReuseAddress(true);
+            serverSocket.setSoTimeout(5_000);
+            setDaemon(true);
+            start();
+        }
+
+        @Override
+        public void run() {
+            try {
+                try (final Socket socket = serverSocket.accept()) {
+                    final InputStream inputStream = socket.getInputStream();
+                    final byte[] buffer = new byte[1024];
+                    // noinspection InfiniteLoopStatement
+                    while (true) {
+                        final int readByteCount = inputStream.read(buffer);
+                        if (readByteCount > 0) {
+                            synchronized (this) {
+                                totalReadByteCount += readByteCount;
+                                outputStream.write(buffer, 0, readByteCount);
+                            }
+                        }
+                    }
+                }
+            } catch (final EOFException ignored) {
+                // Socket is closed.
+            } catch (final Exception error) {
+                if (!closed) {
+                    throw new RuntimeException(error);
+                }
+            }
+        }
+
+        public synchronized byte[] getReceivedBytes() {
+            return outputStream.toByteArray();
+        }
+
+        public synchronized int getTotalReadByteCount() {
+            return totalReadByteCount;
+        }
+
+        @Override
+        public synchronized void close() throws InterruptedException {
+            if (closed) {
+                throw new IllegalStateException("shutdown has already been invoked");
+            }
+            closed = true;
+            interrupt();
+            join(3_000L);
+        }
+
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java
new file mode 100644
index 0000000..80cd2b9
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java
@@ -0,0 +1,1889 @@
+/*
+ * 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.JsonNode;
+import com.fasterxml.jackson.databind.MappingIterator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.MarkerManager;
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.SocketAppender;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.core.layout.ByteBufferDestination;
+import org.apache.logging.log4j.core.lookup.MainMapLookup;
+import org.apache.logging.log4j.core.net.Severity;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalFields;
+import org.apache.logging.log4j.layout.json.template.util.JsonReader;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.layout.json.template.util.MapAccessor;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.message.ObjectMessage;
+import org.apache.logging.log4j.message.SimpleMessage;
+import org.apache.logging.log4j.message.StringMapMessage;
+import org.apache.logging.log4j.test.AvailablePortFinder;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
+import org.apache.logging.log4j.util.StringMap;
+import org.apache.logging.log4j.util.Strings;
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.math.BigDecimal;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SuppressWarnings("DoubleBraceInitialization")
+public class JsonTemplateLayoutTest {
+
+    private static final Configuration CONFIGURATION = new DefaultConfiguration();
+
+    private static final List<LogEvent> LOG_EVENTS = LogEventFixture.createFullLogEvents(5);
+
+    private static final JsonWriter JSON_WRITER = JsonWriter
+            .newBuilder()
+            .setMaxStringLength(10_000)
+            .setTruncatedStringSuffix("…")
+            .build();
+
+    private static final ObjectMapper OBJECT_MAPPER = JacksonFixture.getObjectMapper();
+
+    private static final String LOGGER_NAME = JsonTemplateLayoutTest.class.getSimpleName();
+
+    @Test
+    public void test_serialized_event() throws IOException {
+        final String lookupTestKey = "lookup_test_key";
+        final String lookupTestVal =
+                String.format("lookup_test_value_%d", (int) (1000 * Math.random()));
+        System.setProperty(lookupTestKey, lookupTestVal);
+        for (final LogEvent logEvent : LOG_EVENTS) {
+            checkLogEvent(logEvent, lookupTestKey, lookupTestVal);
+        }
+    }
+
+    private void checkLogEvent(
+            final LogEvent logEvent,
+            @SuppressWarnings("SameParameterValue")
+            final String lookupTestKey,
+            final String lookupTestVal) throws IOException {
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplateUri("classpath:testJsonTemplateLayout.json")
+                .setStackTraceEnabled(true)
+                .setLocationInfoEnabled(true)
+                .build();
+        final String serializedLogEvent = layout.toSerializable(logEvent);
+        final JsonNode rootNode = OBJECT_MAPPER.readValue(serializedLogEvent, JsonNode.class);
+        checkConstants(rootNode);
+        checkBasicFields(logEvent, rootNode);
+        checkSource(logEvent, rootNode);
+        checkException(layout.getCharset(), logEvent, rootNode);
+        checkLookupTest(lookupTestKey, lookupTestVal, rootNode);
+    }
+
+    private static void checkConstants(final JsonNode rootNode) {
+        assertThat(point(rootNode, "@version").asInt()).isEqualTo(1);
+    }
+
+    private static void checkBasicFields(final LogEvent logEvent, final JsonNode rootNode) {
+        assertThat(point(rootNode, "message").asText())
+                .isEqualTo(logEvent.getMessage().getFormattedMessage());
+        assertThat(point(rootNode, "level").asText())
+                .isEqualTo(logEvent.getLevel().name());
+        assertThat(point(rootNode, "logger_fqcn").asText())
+                .isEqualTo(logEvent.getLoggerFqcn());
+        assertThat(point(rootNode, "logger_name").asText())
+                .isEqualTo(logEvent.getLoggerName());
+        assertThat(point(rootNode, "thread_id").asLong())
+                .isEqualTo(logEvent.getThreadId());
+        assertThat(point(rootNode, "thread_name").asText())
+                .isEqualTo(logEvent.getThreadName());
+        assertThat(point(rootNode, "thread_priority").asInt())
+                .isEqualTo(logEvent.getThreadPriority());
+        assertThat(point(rootNode, "end_of_batch").asBoolean())
+                .isEqualTo(logEvent.isEndOfBatch());
+    }
+
+    private static void checkSource(final LogEvent logEvent, final JsonNode rootNode) {
+        assertThat(point(rootNode, "class").asText()).isEqualTo(logEvent.getSource().getClassName());
+        assertThat(point(rootNode, "file").asText()).isEqualTo(logEvent.getSource().getFileName());
+        assertThat(point(rootNode, "line_number").asInt()).isEqualTo(logEvent.getSource().getLineNumber());
+    }
+
+    private static void checkException(
+            final Charset charset,
+            final LogEvent logEvent,
+            final JsonNode rootNode) {
+        final Throwable thrown = logEvent.getThrown();
+        if (thrown != null) {
+            assertThat(point(rootNode, "exception_class").asText()).isEqualTo(thrown.getClass().getCanonicalName());
+            assertThat(point(rootNode, "exception_message").asText()).isEqualTo(thrown.getMessage());
+            final String stackTrace = serializeStackTrace(charset, thrown);
+            assertThat(point(rootNode, "stacktrace").asText()).isEqualTo(stackTrace);
+        }
+    }
+
+    private static String serializeStackTrace(
+            final Charset charset,
+            final Throwable exception) {
+        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        final String charsetName = charset.name();
+        try (final PrintStream printStream =
+                     new PrintStream(outputStream, false, charsetName)) {
+            exception.printStackTrace(printStream);
+            return outputStream.toString(charsetName);
+        }  catch (final UnsupportedEncodingException error) {
+            throw new RuntimeException("failed converting the stack trace to string", error);
+        }
+    }
+
+    private static void checkLookupTest(
+            final String lookupTestKey,
+            final String lookupTestVal,
+            final JsonNode rootNode) {
+        assertThat(point(rootNode, lookupTestKey).asText()).isEqualTo(lookupTestVal);
+    }
+
+    private static JsonNode point(final JsonNode node, final Object... fields) {
+        final String pointer = createJsonPointer(fields);
+        return node.at(pointer);
+    }
+
+    private static String createJsonPointer(final Object... fields) {
+        final StringBuilder jsonPathBuilder = new StringBuilder();
+        for (final Object field : fields) {
+            jsonPathBuilder.append("/").append(field);
+        }
+        return jsonPathBuilder.toString();
+    }
+
+    @Test
+    public void test_inline_template() throws Exception {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World");
+        final String timestamp = "2017-09-28T17:13:29.098+02:00";
+        final long timeMillis = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
+                .parse(timestamp)
+                .getTime();
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .setTimeMillis(timeMillis)
+                .build();
+
+        // Create the event template.
+        final String timestampFieldName = "@timestamp";
+        final String staticFieldName = "staticFieldName";
+        final String staticFieldValue = "staticFieldValue";
+        final String eventTemplate = writeJson(Map(
+                timestampFieldName, Map(
+                        "$resolver", "timestamp",
+                        "pattern", Map("timeZone", "Europe/Amsterdam")),
+                staticFieldName, staticFieldValue));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString(timestampFieldName)).isEqualTo(timestamp);
+            assertThat(accessor.getString(staticFieldName)).isEqualTo(staticFieldValue);
+        });
+
+    }
+
+    @Test
+    public void test_log4j_deferred_runtime_resolver_for_MapMessage() {
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "mapValue3", Map("$resolver", "message"),
+                "mapValue1", "${map:key1}",
+                "mapValue2", "${map:key2}",
+                "nestedLookupEmptyValue", "${map:noExist:-${map:noExist2:-${map:noExist3:-}}}",
+                "nestedLookupStaticValue", "${map:noExist:-${map:noExist2:-${map:noExist3:-Static Value}}}"));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Create the log event with a MapMessage.
+        final StringMapMessage mapMessage = new StringMapMessage()
+                .with("key1", "val1")
+                .with("key2", "val2")
+                .with("key3", Collections.singletonMap("foo", "bar"));
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(mapMessage)
+                .setTimeMillis(System.currentTimeMillis())
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString("mapValue1")).isEqualTo("val1");
+            assertThat(accessor.getString("mapValue2")).isEqualTo("val2");
+            assertThat(accessor.getString("nestedLookupEmptyValue")).isEmpty();
+            assertThat(accessor.getString("nestedLookupStaticValue")).isEqualTo("Static Value");
+        });
+
+    }
+
+    @Test
+    public void test_MapMessage_serialization() {
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "message", Map("$resolver", "message")));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Create the log event with a MapMessage.
+        final StringMapMessage mapMessage = new StringMapMessage()
+                .with("key1", "val1")
+                .with("key2", 0xDEADBEEF)
+                .with("key3", Collections.singletonMap("key3.1", "val3.1"));
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(mapMessage)
+                .setTimeMillis(System.currentTimeMillis())
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString(new String[]{"message", "key1"})).isEqualTo("val1");
+            assertThat(accessor.getInteger(new String[]{"message", "key2"})).isEqualTo(0xDEADBEEF);
+            assertThat(accessor.getString(new String[]{"message", "key3", "key3.1"})).isEqualTo("val3.1");
+        });
+
+    }
+
+    @Test
+    public void test_MapMessage_keyed_access() {
+
+        // Create the event template.
+        final String key = "list";
+        final String eventTemplate = writeJson(Map(
+                "typedValue", Map(
+                        "$resolver", "map",
+                        "key", key),
+                "stringifiedValue", Map(
+                        "$resolver", "map",
+                        "key", key,
+                        "stringified", true)));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Create the log event with a MapMessage.
+        final List<Integer> value = Arrays.asList(1, 2);
+        final StringMapMessage mapMessage = new StringMapMessage()
+                .with(key, value);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(mapMessage)
+                .setTimeMillis(System.currentTimeMillis())
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getObject("typedValue")).isEqualTo(value);
+            assertThat(accessor.getString("stringifiedValue")).isEqualTo(String.valueOf(value));
+        });
+
+    }
+
+    @Test
+    public void test_message_fallbackKey() {
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "message", Map(
+                        "$resolver", "message",
+                        "fallbackKey", "formattedMessage")));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Create a log event with a MapMessage.
+        final Message mapMessage = new StringMapMessage()
+                .with("key1", "val1");
+        final LogEvent mapMessageLogEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(mapMessage)
+                .setTimeMillis(System.currentTimeMillis())
+                .build();
+
+        // Check the serialized MapMessage.
+        usingSerializedLogEventAccessor(layout, mapMessageLogEvent, accessor ->
+                assertThat(accessor.getString(new String[]{"message", "key1"}))
+                        .isEqualTo("val1"));
+
+        // Create a log event with a SimpleMessage.
+        final Message simpleMessage = new SimpleMessage("simple");
+        final LogEvent simpleMessageLogEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(simpleMessage)
+                .setTimeMillis(System.currentTimeMillis())
+                .build();
+
+        // Check the serialized MapMessage.
+        usingSerializedLogEventAccessor(layout, simpleMessageLogEvent, accessor ->
+                assertThat(accessor.getString(new String[]{"message", "formattedMessage"}))
+                        .isEqualTo("simple"));
+
+    }
+
+    @Test
+    public void test_property_injection() {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .build();
+
+        // Create the event template with property.
+        final String propertyName = "propertyName";
+        final String eventTemplate = writeJson(Map(
+                propertyName, "${" + propertyName + "}"));
+
+        // Create the layout with property.
+        final String propertyValue = "propertyValue";
+        final Configuration config = ConfigurationBuilderFactory
+                .newConfigurationBuilder()
+                .addProperty(propertyName, propertyValue)
+                .build();
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(config)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor ->
+                assertThat(accessor.getString(propertyName)).isEqualTo(propertyValue));
+
+    }
+
+    @Test
+    public void test_empty_root_cause() {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final RuntimeException exception = new RuntimeException("failure for test purposes");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.ERROR)
+                .setMessage(message)
+                .setThrown(exception)
+                .build();
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "ex_class", Map(
+                        "$resolver", "exception",
+                        "field", "className"),
+                "ex_message", Map(
+                        "$resolver", "exception",
+                        "field", "message"),
+                "ex_stacktrace", Map(
+                        "$resolver", "exception",
+                        "field", "stackTrace",
+                        "stringified", true),
+                "root_ex_class", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "className"),
+                "root_ex_message", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "message"),
+                "root_ex_stacktrace", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "stackTrace",
+                        "stringified", true)));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setStackTraceEnabled(true)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString("ex_class"))
+                    .isEqualTo(exception.getClass().getCanonicalName());
+            assertThat(accessor.getString("ex_message"))
+                    .isEqualTo(exception.getMessage());
+            assertThat(accessor.getString("ex_stacktrace"))
+                    .startsWith(exception.getClass().getCanonicalName() + ": " + exception.getMessage());
+            assertThat(accessor.getString("root_ex_class"))
+                    .isEqualTo(accessor.getString("ex_class"));
+            assertThat(accessor.getString("root_ex_message"))
+                    .isEqualTo(accessor.getString("ex_message"));
+            assertThat(accessor.getString("root_ex_stacktrace"))
+                    .isEqualTo(accessor.getString("ex_stacktrace"));
+        });
+
+    }
+
+    @Test
+    public void test_root_cause() {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final RuntimeException exceptionCause = new RuntimeException("failure cause for test purposes");
+        final RuntimeException exception = new RuntimeException("failure for test purposes", exceptionCause);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.ERROR)
+                .setMessage(message)
+                .setThrown(exception)
+                .build();
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "ex_class", Map(
+                        "$resolver", "exception",
+                        "field", "className"),
+                "ex_message", Map(
+                        "$resolver", "exception",
+                        "field", "message"),
+                "ex_stacktrace", Map(
+                        "$resolver", "exception",
+                        "field", "stackTrace",
+                        "stringified", true),
+                "root_ex_class", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "className"),
+                "root_ex_message", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "message"),
+                "root_ex_stacktrace", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "stackTrace",
+                        "stringified", true)));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setStackTraceEnabled(true)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString("ex_class"))
+                    .isEqualTo(exception.getClass().getCanonicalName());
+            assertThat(accessor.getString("ex_message"))
+                    .isEqualTo(exception.getMessage());
+            assertThat(accessor.getString("ex_stacktrace"))
+                    .startsWith(exception.getClass().getCanonicalName() + ": " + exception.getMessage());
+            assertThat(accessor.getString("root_ex_class"))
+                    .isEqualTo(exceptionCause.getClass().getCanonicalName());
+            assertThat(accessor.getString("root_ex_message"))
+                    .isEqualTo(exceptionCause.getMessage());
+            assertThat(accessor.getString("root_ex_stacktrace"))
+                    .startsWith(exceptionCause.getClass().getCanonicalName() + ": " + exceptionCause.getMessage());
+        });
+
+    }
+
+    @Test
+    public void test_marker_name() {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final String markerName = "test";
+        final Marker marker = MarkerManager.getMarker(markerName);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.ERROR)
+                .setMessage(message)
+                .setMarker(marker)
+                .build();
+
+        // Create the event template.
+        final String messageKey = "message";
+        final String markerNameKey = "marker";
+        final String eventTemplate = writeJson(Map(
+                "message", Map("$resolver", "message"),
+                "marker", Map(
+                        "$resolver", "marker",
+                        "field", "name")));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString(messageKey)).isEqualTo(message.getFormattedMessage());
+            assertThat(accessor.getString(markerNameKey)).isEqualTo(markerName);
+        });
+
+    }
+
+    @Test
+    public void test_lineSeparator_suffix() {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .build();
+
+        // Check line separators.
+        test_lineSeparator_suffix(logEvent, true);
+        test_lineSeparator_suffix(logEvent, false);
+
+    }
+
+    private void test_lineSeparator_suffix(
+            final LogEvent logEvent,
+            final boolean prettyPrintEnabled) {
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplateUri("classpath:LogstashJsonEventLayoutV1.json")
+                .build();
+
+        // Check the serialized event.
+        final String serializedLogEvent = layout.toSerializable(logEvent);
+        final String assertionCaption = String.format("testing lineSeperator (prettyPrintEnabled=%s)", prettyPrintEnabled);
+        assertThat(serializedLogEvent).as(assertionCaption).endsWith("}" + System.lineSeparator());
+
+    }
+
+    @Test
+    public void test_main_key_access() {
+
+        // Set main() arguments.
+        final String kwKey = "--name";
+        final String kwVal = "aNameValue";
+        final String positionArg = "position2Value";
+        final String missingKwKey = "--missing";
+        final String[] mainArgs = {kwKey, kwVal, positionArg};
+        MainMapLookup.setMainArguments(mainArgs);
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final LogEvent logEvent = Log4jLogEvent
+            .newBuilder()
+            .setLoggerName(LOGGER_NAME)
+            .setLevel(Level.INFO)
+            .setMessage(message)
+            .build();
+
+        // Create the template.
+        final String template = writeJson(Map(
+                "name", Map(
+                        "$resolver", "main",
+                        "key", kwKey),
+                "positionArg", Map(
+                        "$resolver", "main",
+                        "index", 2),
+                "notFoundArg", Map(
+                        "$resolver", "main",
+                        "key", missingKwKey)));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(template)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString("name")).isEqualTo(kwVal);
+            assertThat(accessor.getString("positionArg")).isEqualTo(positionArg);
+            assertThat(accessor.exists("notFoundArg")).isFalse();
+        });
+
+    }
+
+    @Test
+    public void test_mdc_key_access() {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final StringMap contextData = new SortedArrayStringMap();
+        final String mdcDirectlyAccessedKey = "mdcKey1";
+        final String mdcDirectlyAccessedValue = "mdcValue1";
+        contextData.putValue(mdcDirectlyAccessedKey, mdcDirectlyAccessedValue);
+        final String mdcDirectlyAccessedNullPropertyKey = "mdcKey2";
+        final String mdcDirectlyAccessedNullPropertyValue = null;
+        // noinspection ConstantConditions
+        contextData.putValue(mdcDirectlyAccessedNullPropertyKey, mdcDirectlyAccessedNullPropertyValue);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .setContextData(contextData)
+                .build();
+
+        // Create the event template.
+        String eventTemplate = writeJson(Map(
+                mdcDirectlyAccessedKey, Map(
+                        "$resolver", "mdc",
+                        "key", mdcDirectlyAccessedKey),
+                mdcDirectlyAccessedNullPropertyKey, Map(
+                        "$resolver", "mdc",
+                        "key", mdcDirectlyAccessedNullPropertyKey)));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setStackTraceEnabled(true)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString(mdcDirectlyAccessedKey)).isEqualTo(mdcDirectlyAccessedValue);
+            assertThat(accessor.getString(mdcDirectlyAccessedNullPropertyKey)).isNull();
+        });
+
+    }
+
+    @Test
+    public void test_mdc_pattern() {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final StringMap contextData = new SortedArrayStringMap();
+        final String mdcPatternMatchedKey = "mdcKey1";
+        final String mdcPatternMatchedValue = "mdcValue1";
+        contextData.putValue(mdcPatternMatchedKey, mdcPatternMatchedValue);
+        final String mdcPatternMismatchedKey = "mdcKey2";
+        final String mdcPatternMismatchedValue = "mdcValue2";
+        contextData.putValue(mdcPatternMismatchedKey, mdcPatternMismatchedValue);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .setContextData(contextData)
+                .build();
+
+        // Create the event template.
+        final String mdcFieldName = "mdc";
+        final String eventTemplate = writeJson(Map(
+                mdcFieldName, Map(
+                        "$resolver", "mdc",
+                        "pattern", mdcPatternMatchedKey)));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setStackTraceEnabled(true)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString(new String[]{mdcFieldName, mdcPatternMatchedKey})).isEqualTo(mdcPatternMatchedValue);
+            assertThat(accessor.exists(new String[]{mdcFieldName, mdcPatternMismatchedKey})).isFalse();
+        });
+
+    }
+
+    @Test
+    public void test_mdc_flatten() {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final StringMap contextData = new SortedArrayStringMap();
+        final String mdcPatternMatchedKey = "mdcKey1";
+        final String mdcPatternMatchedValue = "mdcValue1";
+        contextData.putValue(mdcPatternMatchedKey, mdcPatternMatchedValue);
+        final String mdcPatternMismatchedKey = "mdcKey2";
+        final String mdcPatternMismatchedValue = "mdcValue2";
+        contextData.putValue(mdcPatternMismatchedKey, mdcPatternMismatchedValue);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .setContextData(contextData)
+                .build();
+
+        // Create the event template.
+        final String mdcPrefix = "_mdc.";
+        final String eventTemplate = writeJson(Map(
+                "ignoredFieldName", Map(
+                        "$resolver", "mdc",
+                        "pattern", mdcPatternMatchedKey,
+                        "flatten", Map("prefix", mdcPrefix))));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setStackTraceEnabled(true)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString(mdcPrefix + mdcPatternMatchedKey)).isEqualTo(mdcPatternMatchedValue);
+            assertThat(accessor.exists(mdcPrefix + mdcPatternMismatchedKey)).isFalse();
+        });
+
+    }
+
+    @Test
+    public void test_MapResolver() {
+
+        // Create the log event.
+        final StringMapMessage message = new StringMapMessage().with("key1", "val1");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .build();
+
+        // Create the event template node with map values.
+        final String eventTemplate = writeJson(Map(
+                "mapValue1", Map(
+                        "$resolver", "map",
+                        "key", "key1"),
+                "mapValue2", Map(
+                        "$resolver", "map",
+                        "key", "key?")));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Check serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString("mapValue1")).isEqualTo("val1");
+            assertThat(accessor.getString("mapValue2")).isNull();
+        });
+
+    }
+
+    @Test
+    public void test_StringMapMessage() {
+
+        // Create the log event.
+        final StringMapMessage message = new StringMapMessage();
+        message.put("message", "Hello, World!");
+        message.put("bottle", "Kickapoo Joy Juice");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .build();
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "message", Map("$resolver", "message")));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setStackTraceEnabled(true)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getString(new String[]{"message", "message"})).isEqualTo("Hello, World!");
+            assertThat(accessor.getString(new String[]{"message", "bottle"})).isEqualTo("Kickapoo Joy Juice");
+        });
+
+    }
+
+    @Test
+    public void test_ObjectMessage() {
+
+        // Create the log event.
+        final int id = 0xDEADBEEF;
+        final String name = "name-" + id;
+        final Object attachment = new LinkedHashMap<String, Object>() {{
+            put("id", id);
+            put("name", name);
+        }};
+        final ObjectMessage message = new ObjectMessage(attachment);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .build();
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "message", Map("$resolver", "message")));
+
+        // Create the layout.
+        JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setStackTraceEnabled(true)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getInteger(new String[]{"message", "id"})).isEqualTo(id);
+            assertThat(accessor.getString(new String[]{"message", "name"})).isEqualTo(name);
+        });
+
+    }
+
+    @Test
+    public void test_StackTraceElement_template() {
+
+        // Create the stack trace element template.
+        final String classNameFieldName = "className";
+        final String methodNameFieldName = "methodName";
+        final String fileNameFieldName = "fileName";
+        final String lineNumberFieldName = "lineNumber";
+        final String stackTraceElementTemplate = writeJson(Map(
+                classNameFieldName, Map(
+                        "$resolver", "stackTraceElement",
+                        "field", "className"),
+                methodNameFieldName, Map(
+                        "$resolver", "stackTraceElement",
+                        "field", "methodName"),
+                fileNameFieldName, Map(
+                        "$resolver", "stackTraceElement",
+                        "field", "fileName"),
+                lineNumberFieldName, Map(
+                        "$resolver", "stackTraceElement",
+                        "field", "lineNumber")));
+
+        // Create the event template.
+        final String stackTraceFieldName = "stackTrace";
+        final String eventTemplate = writeJson(Map(
+                stackTraceFieldName, Map(
+                        "$resolver", "exception",
+                        "field", "stackTrace")));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setStackTraceEnabled(true)
+                .setStackTraceElementTemplate(stackTraceElementTemplate)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final RuntimeException exceptionCause = new RuntimeException("failure cause for test purposes");
+        final RuntimeException exception = new RuntimeException("failure for test purposes", exceptionCause);
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.ERROR)
+                .setMessage(message)
+                .setThrown(exception)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.exists(stackTraceFieldName)).isTrue();
+            @SuppressWarnings("unchecked")
+            final List<Map<String, Object>> deserializedStackTraceElements =
+                    accessor.getObject(stackTraceFieldName, List.class);
+            final StackTraceElement[] stackTraceElements = exception.getStackTrace();
+            assertThat(deserializedStackTraceElements.size()).isEqualTo(stackTraceElements.length);
+            for (int stackTraceElementIndex = 0;
+                 stackTraceElementIndex < stackTraceElements.length;
+                 stackTraceElementIndex++) {
+                final StackTraceElement stackTraceElement = stackTraceElements[stackTraceElementIndex];
+                final Map<String, Object> deserializedStackTraceElement = deserializedStackTraceElements.get(stackTraceElementIndex);
+                assertThat(deserializedStackTraceElement.size()).isEqualTo(4);
+                assertThat(deserializedStackTraceElement.get(classNameFieldName))
+                        .isEqualTo(stackTraceElement.getClassName());
+                assertThat(deserializedStackTraceElement.get(methodNameFieldName))
+                        .isEqualTo(stackTraceElement.getMethodName());
+                assertThat(deserializedStackTraceElement.get(fileNameFieldName))
+                        .isEqualTo(stackTraceElement.getFileName());
+                assertThat(deserializedStackTraceElement.get(lineNumberFieldName))
+                        .isEqualTo(stackTraceElement.getLineNumber());
+            }
+        });
+
+    }
+
+    @Test
+    public void test_toSerializable_toByteArray_encode_outputs() {
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplateUri("classpath:LogstashJsonEventLayoutV1.json")
+                .setStackTraceEnabled(true)
+                .build();
+
+        // Create the log event.
+        final LogEvent logEvent = LogEventFixture.createFullLogEvents(1).get(0);
+
+        // Get toSerializable() output.
+        final String toSerializableOutput = layout.toSerializable(logEvent);
+
+        // Get toByteArrayOutput().
+        final byte[] toByteArrayOutputBytes = layout.toByteArray(logEvent);
+        final String toByteArrayOutput = new String(
+                toByteArrayOutputBytes,
+                0,
+                toByteArrayOutputBytes.length,
+                layout.getCharset());
+
+        // Get encode() output.
+        final ByteBuffer byteBuffer = ByteBuffer.allocate(512 * 1024);
+        final ByteBufferDestination byteBufferDestination = new ByteBufferDestination() {
+
+            @Override
+            public ByteBuffer getByteBuffer() {
+                return byteBuffer;
+            }
+
+            @Override
+            public ByteBuffer drain(final ByteBuffer ignored) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public void writeBytes(final ByteBuffer data) {
+                byteBuffer.put(data);
+            }
+
+            @Override
+            public void writeBytes(final byte[] buffer, final int offset, final int length) {
+                byteBuffer.put(buffer, offset, length);
+            }
+
+        };
+        layout.encode(logEvent, byteBufferDestination);
+        String encodeOutput = new String(
+                byteBuffer.array(),
+                0,
+                byteBuffer.position(),
+                layout.getCharset());
+
+        // Compare outputs.
+        assertThat(toSerializableOutput).isEqualTo(toByteArrayOutput);
+        assertThat(toByteArrayOutput).isEqualTo(encodeOutput);
+
+    }
+
+    @Test
+    public void test_maxStringLength() {
+
+        // Create the log event.
+        final int maxStringLength = 30;
+        final String excessiveMessageString = Strings.repeat("m", maxStringLength) + 'M';
+        final SimpleMessage message = new SimpleMessage(excessiveMessageString);
+        final Throwable thrown = new RuntimeException();
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.INFO)
+                .setMessage(message)
+                .setThrown(thrown)
+                .build();
+
+        // Create the event template node with map values.
+        final String messageKey = "message";
+        final String excessiveKey = Strings.repeat("k", maxStringLength) + 'K';
+        final String excessiveValue = Strings.repeat("v", maxStringLength) + 'V';
+        final String nullValueKey = "nullValueKey";
+        final String eventTemplate = writeJson(Map(
+                messageKey, Map("$resolver", "message"),
+                excessiveKey, excessiveValue,
+                nullValueKey, Map(
+                        "$resolver", "exception",
+                        "field", "message")));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .setMaxStringLength(maxStringLength)
+                .build();
+
+        // Check serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            final String truncatedStringSuffix =
+                    JsonTemplateLayoutDefaults.getTruncatedStringSuffix();
+            final String truncatedMessageString =
+                    excessiveMessageString.substring(0, maxStringLength) +
+                            truncatedStringSuffix;
+            assertThat(accessor.getString(messageKey)).isEqualTo(truncatedMessageString);
+            final String truncatedKey =
+                    excessiveKey.substring(0, maxStringLength) +
+                            truncatedStringSuffix;
+            final String truncatedValue =
+                    excessiveValue.substring(0, maxStringLength) +
+                            truncatedStringSuffix;
+            assertThat(accessor.getString(truncatedKey)).isEqualTo(truncatedValue);
+            assertThat(accessor.getString(nullValueKey)).isNull();
+        });
+
+    }
+
+    private static final class NonAsciiUtf8MethodNameContainingException extends RuntimeException {;
+
+        public static final long serialVersionUID = 0;
+
+        private static final String NON_ASCII_UTF8_TEXT = "அஆஇฬ๘";
+
+        private static final NonAsciiUtf8MethodNameContainingException INSTANCE =
+                createInstance();
+
+        private static NonAsciiUtf8MethodNameContainingException createInstance() {
+            try {
+                throwException_அஆஇฬ๘();
+                throw new IllegalStateException("should not have reached here");
+            } catch (final NonAsciiUtf8MethodNameContainingException exception) {
+                return exception;
+            }
+        }
+
+        @SuppressWarnings("NonAsciiCharacters")
+        private static void throwException_அஆஇฬ๘() {
+            throw new NonAsciiUtf8MethodNameContainingException(
+                    "exception with non-ASCII UTF-8 method name");
+        }
+
+        private NonAsciiUtf8MethodNameContainingException(final String message) {
+            super(message);
+        }
+
+    }
+
+    @Test
+    public void test_exception_with_nonAscii_utf8_method_name() {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final RuntimeException exception = NonAsciiUtf8MethodNameContainingException.INSTANCE;
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(Level.ERROR)
+                .setMessage(message)
+                .setThrown(exception)
+                .build();
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "ex_stacktrace", Map(
+                        "$resolver", "exception",
+                        "field", "stackTrace",
+                        "stringified", true)));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setStackTraceEnabled(true)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor ->
+                assertThat(accessor.getString("ex_stacktrace"))
+                        .contains(NonAsciiUtf8MethodNameContainingException.NON_ASCII_UTF8_TEXT));
+
+    }
+
+    @Test
+    public void test_event_template_additional_fields() {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final RuntimeException exception = NonAsciiUtf8MethodNameContainingException.INSTANCE;
+        final Level level = Level.ERROR;
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setLevel(level)
+                .setMessage(message)
+                .setThrown(exception)
+                .build();
+
+        // Create the event template.
+        final String eventTemplate = "{}";
+
+        // Create the layout.
+        final EventTemplateAdditionalField[] additionalFieldPairs = {
+                EventTemplateAdditionalField
+                        .newBuilder()
+                        .setKey("number")
+                        .setValue("1")
+                        .setType(EventTemplateAdditionalField.Type.JSON)
+                        .build(),
+                EventTemplateAdditionalField
+                        .newBuilder()
+                        .setKey("string")
+                        .setValue("foo")
+                        .build(),
+                EventTemplateAdditionalField
+                        .newBuilder()
+                        .setKey("level")
+                        .setValue("{\"$resolver\": \"level\", \"field\": \"name\"}")
+                        .setType(EventTemplateAdditionalField.Type.JSON)
+                        .build()
+        };
+        final EventTemplateAdditionalFields additionalFields = EventTemplateAdditionalFields
+                .newBuilder()
+                .setAdditionalFields(additionalFieldPairs)
+                .build();
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setStackTraceEnabled(true)
+                .setEventTemplate(eventTemplate)
+                .setEventTemplateAdditionalFields(additionalFields)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getInteger("number")).isEqualTo(1);
+            assertThat(accessor.getString("string")).isEqualTo("foo");
+            assertThat(accessor.getString("level")).isEqualTo(level.name());
+        });
+
+    }
+
+    @Test
+    @SuppressWarnings("FloatingPointLiteralPrecision")
+    public void test_timestamp_epoch_resolvers() {
+
+        final List<Map<String, Object>> testCases = Arrays.asList(
+                Map(
+                        "epochSecs", new BigDecimal("1581082727.982123456"),
+                        "epochSecsRounded", 1581082727,
+                        "epochSecsNanos", 982123456,
+                        "epochMillis", new BigDecimal("1581082727982.123456"),
+                        "epochMillisRounded", 1581082727982L,
+                        "epochMillisNanos", 123456,
+                        "epochNanos", 1581082727982123456L),
+                Map(
+                        "epochSecs", new BigDecimal("1591177590.005000001"),
+                        "epochSecsRounded", 1591177590,
+                        "epochSecsNanos", 5000001,
+                        "epochMillis", new BigDecimal("1591177590005.000001"),
+                        "epochMillisRounded", 1591177590005L,
+                        "epochMillisNanos", 1,
+                        "epochNanos", 1591177590005000001L));
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "epochSecs", Map(
+                        "$resolver", "timestamp",
+                        "epoch", Map("unit", "secs")),
+                "epochSecsRounded", Map(
+                        "$resolver", "timestamp",
+                        "epoch", Map(
+                                "unit", "secs",
+                                "rounded", true)),
+                "epochSecsNanos", Map(
+                        "$resolver", "timestamp",
+                        "epoch", Map("unit", "secs.nanos")),
+                "epochMillis", Map(
+                        "$resolver", "timestamp",
+                        "epoch", Map("unit", "millis")),
+                "epochMillisRounded", Map(
+                        "$resolver", "timestamp",
+                        "epoch", Map(
+                                "unit", "millis",
+                                "rounded", true)),
+                "epochMillisNanos", Map(
+                        "$resolver", "timestamp",
+                        "epoch", Map("unit", "millis.nanos")),
+                "epochNanos", Map(
+                        "$resolver", "timestamp",
+                        "epoch", Map("unit", "nanos"))));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        testCases.forEach(testCase -> {
+
+            // Create the log event.
+            final SimpleMessage message = new SimpleMessage("Hello, World!");
+            final Level level = Level.ERROR;
+            final MutableInstant instant = new MutableInstant();
+            final Object instantSecsObject = testCase.get("epochSecsRounded");
+            final long instantSecs = instantSecsObject instanceof Long
+                    ? (long) instantSecsObject
+                    : (int) instantSecsObject;
+            final int instantSecsNanos = (int) testCase.get("epochSecsNanos");
+            instant.initFromEpochSecond(instantSecs, instantSecsNanos);
+            final LogEvent logEvent = Log4jLogEvent
+                    .newBuilder()
+                    .setLoggerName(LOGGER_NAME)
+                    .setLevel(level)
+                    .setMessage(message)
+                    .setInstant(instant)
+                    .build();
+
+            // Verify the test case.
+            usingSerializedLogEventAccessor(layout, logEvent, accessor ->
+                    testCase.forEach((key, expectedValue) ->
+                            Assertions
+                                    .assertThat(accessor.getObject(key))
+                                    .describedAs("key=%s", key)
+                                    .isEqualTo(expectedValue)));
+
+        });
+
+    }
+
+    @Test
+    public void test_timestamp_pattern_resolver() {
+
+        // Create log events.
+        final String logEvent1FormattedInstant = "2019-01-02T09:34:11Z";
+        final LogEvent logEvent1 = createLogEventAtInstant(logEvent1FormattedInstant);
+        final String logEvent2FormattedInstant = "2019-01-02T09:34:12Z";
+        final LogEvent logEvent2 = createLogEventAtInstant(logEvent2FormattedInstant);
+        @SuppressWarnings("UnnecessaryLocalVariable")
+        final String logEvent3FormattedInstant = logEvent2FormattedInstant;
+        final LogEvent logEvent3 = createLogEventAtInstant(logEvent3FormattedInstant);
+        final String logEvent4FormattedInstant = "2019-01-02T09:34:13Z";
+        final LogEvent logEvent4 = createLogEventAtInstant(logEvent4FormattedInstant);
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "timestamp", Map(
+                        "$resolver", "timestamp",
+                        "pattern", Map(
+                                "format", "yyyy-MM-dd'T'HH:mm:ss'Z'",
+                                "timeZone", "UTC"))));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Check the serialized 1st event.
+        usingSerializedLogEventAccessor(layout, logEvent1, accessor ->
+                assertThat(accessor.getString("timestamp"))
+                        .isEqualTo(logEvent1FormattedInstant));
+
+        // Check the serialized 2nd event.
+        usingSerializedLogEventAccessor(layout, logEvent2, accessor ->
+                assertThat(accessor.getString("timestamp"))
+                        .isEqualTo(logEvent2FormattedInstant));
+
+        // Check the serialized 3rd event.
+        usingSerializedLogEventAccessor(layout, logEvent3, accessor ->
+                assertThat(accessor.getString("timestamp"))
+                        .isEqualTo(logEvent3FormattedInstant));
+
+        // Check the serialized 4th event.
+        usingSerializedLogEventAccessor(layout, logEvent4, accessor ->
+                assertThat(accessor.getString("timestamp"))
+                        .isEqualTo(logEvent4FormattedInstant));
+
+    }
+
+    private static LogEvent createLogEventAtInstant(final String formattedInstant) {
+        final SimpleMessage message = new SimpleMessage("LogEvent at instant " + formattedInstant);
+        final long instantEpochMillis = Instant.parse(formattedInstant).toEpochMilli();
+        final MutableInstant instant = new MutableInstant();
+        instant.initFromEpochMilli(instantEpochMillis, 0);
+        return Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setMessage(message)
+                .setInstant(instant)
+                .build();
+    }
+
+    @Test
+    public void test_level_severity() {
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "severityKeyword", Map(
+                        "$resolver", "level",
+                        "field", "severity",
+                        "severity", Map("field", "keyword")),
+                "severityCode", Map(
+                        "$resolver", "level",
+                        "field", "severity",
+                        "severity", Map("field", "code"))));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        for (final Level level : Level.values()) {
+
+            // Create the log event.
+            final SimpleMessage message = new SimpleMessage("Hello, World!");
+            final LogEvent logEvent = Log4jLogEvent
+                    .newBuilder()
+                    .setLoggerName(LOGGER_NAME)
+                    .setLevel(level)
+                    .setMessage(message)
+                    .build();
+
+            // Check the serialized event.
+            usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+                final Severity expectedSeverity = Severity.getSeverity(level);
+                final String expectedSeverityKeyword = expectedSeverity.name();
+                final int expectedSeverityCode = expectedSeverity.getCode();
+                assertThat(accessor.getString("severityKeyword")).isEqualTo(expectedSeverityKeyword);
+                assertThat(accessor.getInteger("severityCode")).isEqualTo(expectedSeverityCode);
+            });
+
+        }
+
+    }
+
+    @Test
+    public void test_exception_resolvers_against_no_exceptions() {
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("Hello, World!");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setMessage(message)
+                .build();
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "exStackTrace", Map(
+                        "$resolver", "exception",
+                        "field", "stackTrace"),
+                "exStackTraceString", Map(
+                        "$resolver", "exception",
+                        "field", "stackTrace",
+                        "stringified", true),
+                "exRootCauseStackTrace", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "stackTrace"),
+                "exRootCauseStackTraceString", Map(
+                        "$resolver", "exceptionRootCause",
+                        "field", "stackTrace",
+                        "stringified", true),
+                "requiredFieldTriggeringError", true));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .setStackTraceEnabled(true)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            assertThat(accessor.getObject("exStackTrace")).isNull();
+            assertThat(accessor.getObject("exStackTraceString")).isNull();
+            assertThat(accessor.getObject("exRootCauseStackTrace")).isNull();
+            assertThat(accessor.getObject("exRootCauseStackTraceString")).isNull();
+            assertThat(accessor.getBoolean("requiredFieldTriggeringError")).isTrue();
+        });
+
+    }
+
+    @Test
+    public void test_StackTraceTextResolver_with_maxStringLength() {
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "stackTrace", Map(
+                        "$resolver", "exception",
+                        "field", "stackTrace",
+                        "stringified", true)));
+
+        // Create the layout.
+        final int maxStringLength = eventTemplate.length();
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .setMaxStringLength(maxStringLength)
+                .setStackTraceEnabled(true)
+                .build();
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("foo");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setMessage(message)
+                .setThrown(NonAsciiUtf8MethodNameContainingException.INSTANCE)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            final int expectedLength = maxStringLength +
+                    JsonTemplateLayoutDefaults.getTruncatedStringSuffix().length();
+            assertThat(accessor.getString("stackTrace").length()).isEqualTo(expectedLength);
+        });
+
+    }
+
+    @Test
+    public void test_null_eventDelimiter() {
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map("key", "val"));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .setEventDelimiter("\0")
+                .build();
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("foo");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setMessage(message)
+                .setThrown(NonAsciiUtf8MethodNameContainingException.INSTANCE)
+                .build();
+
+        // Check the serialized event.
+        final String serializedLogEvent = layout.toSerializable(logEvent);
+        assertThat(serializedLogEvent).isEqualTo(eventTemplate + '\0');
+
+    }
+
+    @Test
+    public void test_against_SocketAppender() throws Exception {
+
+        // Craft nasty events.
+        final List<LogEvent> logEvents = createNastyLogEvents();
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "message", Map("$resolver", "message")));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Create the server.
+        final int port = AvailablePortFinder.getNextAvailable();
+        try (final JsonAcceptingTcpServer server = new JsonAcceptingTcpServer(port, 1)) {
+
+            // Create the appender.
+            final SocketAppender appender = SocketAppender
+                    .newBuilder()
+                    .setHost("localhost")
+                    .setBufferedIo(false)
+                    .setPort(port)
+                    .setReconnectDelayMillis(100)
+                    .setName("test")
+                    .setImmediateFail(false)
+                    .setIgnoreExceptions(false)
+                    .setLayout(layout)
+                    .build();
+
+            // Start the appender.
+            appender.start();
+
+            // Transfer and verify the log events.
+            for (int logEventIndex = 0; logEventIndex < logEvents.size(); logEventIndex++) {
+
+                // Send the log event.
+                final LogEvent logEvent = logEvents.get(logEventIndex);
+                appender.append(logEvent);
+                appender.getManager().flush();
+
+                // Pull the parsed log event.
+                final JsonNode node = server.receivedNodes.poll(3, TimeUnit.SECONDS);
+                assertThat(node)
+                        .as("logEventIndex=%d", logEventIndex)
+                        .isNotNull();
+
+                // Verify the received content.
+                final String expectedMessage = logEvent.getMessage().getFormattedMessage();
+                final String expectedMessageChars = explainChars(expectedMessage);
+                final String actualMessage = point(node, "message").asText();
+                final String actualMessageChars = explainChars(actualMessage);
+                assertThat(actualMessageChars)
+                        .as("logEventIndex=%d", logEventIndex)
+                        .isEqualTo(expectedMessageChars);
+
+            }
+
+            // Verify that there were no overflows.
+            assertThat(server.droppedNodeCount).isZero();
+
+        }
+
+    }
+
+    private static List<LogEvent> createNastyLogEvents() {
+        return createNastyMessages()
+                .stream()
+                .map(message -> Log4jLogEvent
+                        .newBuilder()
+                        .setLoggerName(LOGGER_NAME)
+                        .setMessage(message)
+                        .build())
+                .collect(Collectors.toList());
+    }
+
+    private static List<SimpleMessage> createNastyMessages() {
+
+        // Determine the message count and character offset.
+        final int messageCount = 1024;
+        final int minChar = Character.MIN_VALUE;
+        final int maxChar = Character.MIN_HIGH_SURROGATE - 1;
+        final int totalCharCount = maxChar - minChar + 1;
+        final int charOffset = totalCharCount / messageCount;
+
+        // Populate messages.
+        List<SimpleMessage> messages = new ArrayList<>(messageCount);
+        for (int messageIndex = 0; messageIndex < messageCount; messageIndex++) {
+            final StringBuilder stringBuilder = new StringBuilder(messageIndex + "@");
+            for (int charIndex = 0; charIndex < charOffset; charIndex++) {
+                final char c = (char) (minChar + messageIndex * charOffset + charIndex);
+                stringBuilder.append(c);
+            }
+            final String messageString = stringBuilder.toString();
+            final SimpleMessage message = new SimpleMessage(messageString);
+            messages.add(message);
+        }
+        return messages;
+
+    }
+
+    private static final class JsonAcceptingTcpServer extends Thread implements AutoCloseable {
+
+        private final ServerSocket serverSocket;
+
+        private final BlockingQueue<JsonNode> receivedNodes;
+
+        private volatile int droppedNodeCount = 0;
+
+        private volatile boolean closed = false;
+
+        private JsonAcceptingTcpServer(
+                final int port,
+                final int capacity) throws IOException {
+            this.serverSocket = new ServerSocket(port);
+            this.receivedNodes = new ArrayBlockingQueue<>(capacity);
+            serverSocket.setReuseAddress(true);
+            serverSocket.setSoTimeout(5_000);
+            setDaemon(true);
+            start();
+        }
+
+        @Override
+        public void run() {
+            try {
+                try (final Socket socket = serverSocket.accept()) {
+                    final InputStream inputStream = socket.getInputStream();
+                    while (!closed) {
+                        final MappingIterator<JsonNode> iterator = JacksonFixture
+                                .getObjectMapper()
+                                .readerFor(JsonNode.class)
+                                .readValues(inputStream);
+                        while (iterator.hasNextValue()) {
+                            final JsonNode value = iterator.nextValue();
+                            synchronized (this) {
+                                final boolean added = receivedNodes.offer(value);
+                                if (!added) {
+                                    droppedNodeCount++;
+                                }
+                            }
+                        }
+                    }
+                }
+            } catch (final EOFException ignored) {
+                // Socket is closed.
+            } catch (final Exception error) {
+                if (!closed) {
+                    throw new RuntimeException(error);
+                }
+            }
+        }
+
+        @Override
+        public synchronized void close() throws InterruptedException {
+            if (closed) {
+                throw new IllegalStateException("shutdown has already been invoked");
+            }
+            closed = true;
+            interrupt();
+            join(3_000L);
+        }
+
+    }
+
+    private static String explainChars(final String input) {
+        return IntStream
+                .range(0, input.length())
+                .mapToObj(i -> {
+                    final char c = input.charAt(i);
+                    return String.format("'%c' (%04X)", c, (int) c);
+                })
+                .collect(Collectors.joining(", "));
+    }
+
+    @Test
+    public void test_PatternResolver() {
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "message", Map(
+                        "$resolver", "pattern",
+                        "pattern", "%p:%m")));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .build();
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("foo");
+        final Level level = Level.FATAL;
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setMessage(message)
+                .setLevel(level)
+                .build();
+
+        // Check the serialized event.
+        usingSerializedLogEventAccessor(layout, logEvent, accessor -> {
+            final String expectedMessage = String.format(
+                    "%s:%s",
+                    level, message.getFormattedMessage());
+            assertThat(accessor.getString("message")).isEqualTo(expectedMessage);
+        });
+
+    }
+
+    @Test
+    public void test_unresolvable_nested_fields_are_skipped() {
+
+        // Create the event template.
+        final String eventTemplate = writeJson(Map(
+                "exception", Map(
+                        "message", Map(
+                                "$resolver", "exception",
+                                "field", "message"),
+                        "className", Map(
+                                "$resolver", "exception",
+                                "field", "className")),
+                "exceptionRootCause", Map(
+                        "message", Map(
+                                "$resolver", "exceptionRootCause",
+                                "field", "message"),
+                        "className", Map(
+                                "$resolver", "exceptionRootCause",
+                                "field", "className")),
+                "source", Map(
+                        "lineNumber", Map(
+                                "$resolver", "source",
+                                "field", "lineNumber"),
+                        "fileName", Map(
+                                "$resolver", "source",
+                                "field", "fileName")),
+                "emptyMap", Collections.emptyMap(),
+                "emptyList", Collections.emptyList(),
+                "null", null));
+
+        // Create the layout.
+        final JsonTemplateLayout layout = JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setEventTemplate(eventTemplate)
+                .setStackTraceEnabled(false)        // Disable "exception" and "exceptionRootCause" resolvers.
+                .setLocationInfoEnabled(false)      // Disable the "source" resolver.
+                .build();
+
+        // Create the log event.
+        final SimpleMessage message = new SimpleMessage("foo");
+        final Level level = Level.FATAL;
+        final Exception thrown = new RuntimeException("bar");
+        final LogEvent logEvent = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(LOGGER_NAME)
+                .setMessage(message)
+                .setLevel(level)
+                .setThrown(thrown)
+                .build();
+
+        // Check the serialized event.
+        final String expectedSerializedLogEventJson =
+                "{}" + JsonTemplateLayoutDefaults.getEventDelimiter();
+        final String actualSerializedLogEventJson = layout.toSerializable(logEvent);
+        Assertions
+                .assertThat(actualSerializedLogEventJson)
+                .isEqualTo(expectedSerializedLogEventJson);
+
+    }
+
+    private static String writeJson(final Object value) {
+        final StringBuilder stringBuilder = JSON_WRITER.getStringBuilder();
+        stringBuilder.setLength(0);
+        try {
+            JSON_WRITER.writeValue(value);
+            return stringBuilder.toString();
+        } finally {
+            stringBuilder.setLength(0);
+        }
+    }
+
+    private static void usingSerializedLogEventAccessor(
+            final Layout<String> layout,
+            final LogEvent logEvent,
+            final Consumer<MapAccessor> accessorConsumer) {
+        final String serializedLogEventJson = layout.toSerializable(logEvent);
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> serializedLogEvent =
+                (Map<String, Object>) readJson(serializedLogEventJson);
+        final MapAccessor serializedLogEventAccessor = new MapAccessor(serializedLogEvent);
+        accessorConsumer.accept(serializedLogEventAccessor);
+    }
+
+    private static Object readJson(final String json) {
+        return JsonReader.read(json);
+    }
+
+    private static Map<String, Object> Map(final Object... pairs) {
+        final Map<String, Object> map = new LinkedHashMap<>();
+        if (pairs.length % 2 != 0) {
+            throw new IllegalArgumentException("odd number of arguments");
+        }
+        for (int i = 0; i < pairs.length; i += 2) {
+            final String key = (String) pairs[i];
+            final Object value = pairs[i + 1];
+            map.put(key, value);
+        }
+        return map;
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LayoutComparisonHelpers.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LayoutComparisonHelpers.java
new file mode 100644
index 0000000..6f0a6c0
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LayoutComparisonHelpers.java
@@ -0,0 +1,19 @@
+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.layout.json.template.util.JsonReader;
+
+import java.util.Map;
+
+enum LayoutComparisonHelpers {;
+
+    @SuppressWarnings("unchecked")
+    static Map<String, Object> renderUsing(
+            final LogEvent logEvent,
+            final Layout<String> layout) {
+        final String json = layout.toSerializable(logEvent);
+        return (Map<String, Object>) JsonReader.read(json);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogEventFixture.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogEventFixture.java
new file mode 100644
index 0000000..1b0975a
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogEventFixture.java
@@ -0,0 +1,151 @@
+/*
+ * 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.Level;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.impl.ContextDataFactory;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.message.SimpleMessage;
+import org.apache.logging.log4j.spi.MutableThreadContextStack;
+import org.apache.logging.log4j.spi.ThreadContextStack;
+import org.apache.logging.log4j.util.StringMap;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+enum LogEventFixture {;
+
+    private static final int TIME_OVERLAPPING_CONSECUTIVE_EVENT_COUNT = 10;
+
+    static List<LogEvent> createLiteLogEvents(final int logEventCount) {
+        final List<LogEvent> logEvents = new ArrayList<>(logEventCount);
+        final long startTimeMillis = System.currentTimeMillis();
+        for (int logEventIndex = 0; logEventIndex < logEventCount; logEventIndex++) {
+            final String logEventId = String.valueOf(logEventIndex);
+            final long logEventTimeMillis = createLogEventTimeMillis(startTimeMillis, logEventIndex);
+            final LogEvent logEvent = LogEventFixture.createLiteLogEvent(logEventId, logEventTimeMillis);
+            logEvents.add(logEvent);
+        }
+        return logEvents;
+    }
+
+    private static LogEvent createLiteLogEvent(final String id, final long timeMillis) {
+        final SimpleMessage message = new SimpleMessage("lite LogEvent message " + id);
+        final Level level = Level.DEBUG;
+        final String loggerFqcn = "f.q.c.n" + id;
+        final String loggerName = "a.B" + id;
+        final long nanoTime = timeMillis * 2;
+        return Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(loggerName)
+                .setLoggerFqcn(loggerFqcn)
+                .setLevel(level)
+                .setMessage(message)
+                .setTimeMillis(timeMillis)
+                .setNanoTime(nanoTime)
+                .build();
+    }
+
+    static List<LogEvent> createFullLogEvents(final int logEventCount) {
+        final List<LogEvent> logEvents = new ArrayList<>(logEventCount);
+        final long startTimeMillis = System.currentTimeMillis();
+        for (int logEventIndex = 0; logEventIndex < logEventCount; logEventIndex++) {
+            final String logEventId = String.valueOf(logEventIndex);
+            final long logEventTimeMillis = createLogEventTimeMillis(startTimeMillis, logEventIndex);
+            final LogEvent logEvent = LogEventFixture.createFullLogEvent(logEventId, logEventTimeMillis);
+            logEvents.add(logEvent);
+        }
+        return logEvents;
+    }
+
+    private static long createLogEventTimeMillis(
+            final long startTimeMillis,
+            final int logEventIndex) {
+        // Create event time repeating every certain number of consecutive
+        // events. This is better aligned with the real-world use case and
+        // gives surface to timestamp formatter caches to perform their
+        // magic, which is implemented for almost all layouts.
+        return startTimeMillis + logEventIndex / TIME_OVERLAPPING_CONSECUTIVE_EVENT_COUNT;
+    }
+
+    private static LogEvent createFullLogEvent(
+            final String id,
+            final long timeMillis) {
+
+        // Create exception.
+        final Exception sourceHelper = new Exception();
+        sourceHelper.fillInStackTrace();
+        final Exception cause = new NullPointerException("testNPEx-" + id);
+        sourceHelper.fillInStackTrace();
+        final StackTraceElement source = sourceHelper.getStackTrace()[0];
+        final IOException ioException = new IOException("testIOEx-" + id, cause);
+        ioException.addSuppressed(new IndexOutOfBoundsException("I am suppressed exception 1" + id));
+        ioException.addSuppressed(new IndexOutOfBoundsException("I am suppressed exception 2" + id));
+
+        // Create rest of the event attributes.
+        final SimpleMessage message = new SimpleMessage("full LogEvent message " + id);
+        final StringMap contextData = createContextData(id);
+        final ThreadContextStack contextStack = createContextStack(id);
+        final int threadId = id.hashCode();
+        final String threadName = "MyThreadName" + id;
+        final int threadPriority = threadId % 10;
+        final Level level = Level.DEBUG;
+        final String loggerFqcn = "f.q.c.n" + id;
+        final String loggerName = "a.B" + id;
+        final long nanoTime = timeMillis * 2;
+
+        // Create the event.
+        return Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(loggerName)
+                .setLoggerFqcn(loggerFqcn)
+                .setLevel(level)
+                .setMessage(message)
+                .setThrown(ioException)
+                .setContextData(contextData)
+                .setContextStack(contextStack)
+                .setThreadId(threadId)
+                .setThreadName(threadName)
+                .setThreadPriority(threadPriority)
+                .setSource(source)
+                .setTimeMillis(timeMillis)
+                .setNanoTime(nanoTime)
+                .build();
+
+    }
+
+    private static StringMap createContextData(final String id) {
+        final StringMap contextData = ContextDataFactory.createContextData();
+        contextData.putValue("MDC.String." + id, "String");
+        contextData.putValue("MDC.BigDecimal." + id, BigDecimal.valueOf(Math.PI));
+        contextData.putValue("MDC.Integer." + id, 10);
+        contextData.putValue("MDC.Long." + id, Long.MAX_VALUE);
+        return contextData;
+    }
+
+    private static ThreadContextStack createContextStack(final String id) {
+        final ThreadContextStack contextStack = new MutableThreadContextStack();
+        contextStack.clear();
+        contextStack.push("stack_msg1" + id);
+        contextStack.add("stack_msg2" + id);
+        return contextStack;
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogstashIT.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogstashIT.java
new file mode 100644
index 0000000..311b0df
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/LogstashIT.java
@@ -0,0 +1,503 @@
+package org.apache.logging.log4j.layout.json.template;
+
+import co.elastic.logging.log4j2.EcsLayout;
+import org.apache.http.HttpHost;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.SocketAppender;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.core.layout.GelfLayout;
+import org.apache.logging.log4j.core.util.NetUtils;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
+import org.apache.logging.log4j.layout.json.template.util.ThreadLocalRecyclerFactory;
+import org.apache.logging.log4j.message.SimpleMessage;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.assertj.core.api.Assertions;
+import org.awaitility.Awaitility;
+import org.elasticsearch.ElasticsearchStatusException;
+import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
+import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
+import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
+import org.elasticsearch.action.search.SearchRequest;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.client.RestClientBuilder;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.elasticsearch.cluster.health.ClusterHealthStatus;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.search.SearchHit;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class LogstashIT {
+
+    private static final StatusLogger LOGGER = StatusLogger.getLogger();
+
+    private static final DefaultConfiguration CONFIGURATION = new DefaultConfiguration();
+
+    private static final Charset CHARSET = StandardCharsets.UTF_8;
+
+    private static final String HOST_NAME = NetUtils.getLocalHostname();
+
+    private static final String SERVICE_NAME = "LogstashIT";
+
+    private static final String EVENT_DATASET = SERVICE_NAME + ".log";
+
+    private static final GelfLayout GELF_LAYOUT = GelfLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .setCharset(CHARSET)
+            .setCompressionType(GelfLayout.CompressionType.OFF)
+            .setIncludeNullDelimiter(true)
+            .setHost(HOST_NAME)
+            .build();
+
+    private static final JsonTemplateLayout JSON_TEMPLATE_GELF_LAYOUT = JsonTemplateLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .setCharset(CHARSET)
+            .setEventTemplateUri("classpath:GelfLayout.json")
+            .setEventDelimiter("\0")
+            .setEventTemplateAdditionalFields(JsonTemplateLayout
+                    .EventTemplateAdditionalFields
+                    .newBuilder()
+                    .setAdditionalFields(
+                            new EventTemplateAdditionalField[]{
+                                    EventTemplateAdditionalField
+                                            .newBuilder()
+                                            .setKey("host")
+                                            .setValue(HOST_NAME)
+                                            .build()
+                            })
+                    .build())
+            .build();
+
+    private static final EcsLayout ECS_LAYOUT = EcsLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .setCharset(CHARSET)
+            .setServiceName(SERVICE_NAME)
+            .setEventDataset(EVENT_DATASET)
+            .build();
+
+    private static final JsonTemplateLayout JSON_TEMPLATE_ECS_LAYOUT = JsonTemplateLayout
+            .newBuilder()
+            .setConfiguration(CONFIGURATION)
+            .setCharset(CHARSET)
+            .setEventTemplateUri("classpath:EcsLayout.json")
+            .setRecyclerFactory(ThreadLocalRecyclerFactory.getInstance())
+            .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 int LOG_EVENT_COUNT = 100;
+
+    private static final String ES_INDEX_MESSAGE_FIELD_NAME = "message";
+
+    /**
+     * Constants hardcoded in docker-maven-plugin configuration, do not change!
+     */
+    private enum MavenHardcodedConstants {;
+
+        private static final int LS_GELF_INPUT_PORT = 12222;
+
+        private static final int LS_TCP_INPUT_PORT = 12345;
+
+        private static final int ES_PORT = 9200;
+
+        private static final String ES_INDEX_NAME = "log4j";
+
+    }
+
+    @Test
+    public void test_lite_events() throws IOException {
+        final List<LogEvent> logEvents =
+                LogEventFixture.createLiteLogEvents(LOG_EVENT_COUNT);
+        testEvents(logEvents);
+    }
+
+    @Test
+    public void test_full_events() throws IOException {
+        final List<LogEvent> logEvents =
+                LogEventFixture.createFullLogEvents(LOG_EVENT_COUNT);
+        testEvents(logEvents);
+    }
+
+    private static void testEvents(final List<LogEvent> logEvents) throws IOException {
+        try (final RestHighLevelClient client = createClient()) {
+            final Appender appender = createStartedAppender(
+                    JSON_TEMPLATE_GELF_LAYOUT,
+                    MavenHardcodedConstants.LS_GELF_INPUT_PORT);
+            try {
+
+                // Append events.
+                LOGGER.info("appending events");
+                logEvents.forEach(appender::append);
+                LOGGER.info("completed appending events");
+
+                // Wait all messages to arrive.
+                Awaitility
+                        .await()
+                        .atMost(Duration.ofSeconds(60))
+                        .pollDelay(Duration.ofSeconds(2))
+                        .until(() -> queryDocumentCount(client) == LOG_EVENT_COUNT);
+
+                // Verify indexed messages.
+                final Set<String> expectedMessages = logEvents
+                        .stream()
+                        .map(LogstashIT::expectedLogstashMessageField)
+                        .collect(Collectors.toSet());
+                final Set<String> actualMessages = queryDocuments(client)
+                        .stream()
+                        .map(source -> (String) source.get(ES_INDEX_MESSAGE_FIELD_NAME))
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toSet());
+                Assertions
+                        .assertThat(actualMessages)
+                        .isEqualTo(expectedMessages);
+
+            } finally {
+                appender.stop();
+            }
+        }
+    }
+
+    private static String expectedLogstashMessageField(final LogEvent logEvent) {
+        final Throwable throwable = logEvent.getThrown();
+        if (throwable != null) {
+            try (final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+                 final PrintStream printStream = new PrintStream(outputStream, false, CHARSET.name())) {
+                throwable.printStackTrace(printStream);
+                return outputStream.toString(CHARSET.name());
+            } catch (final Exception error) {
+                throw new RuntimeException(
+                        "failed printing stack trace",
+                        error);
+            }
+        } else {
+            return logEvent.getMessage().getFormattedMessage();
+        }
+    }
+
+    @Test
+    public void test_newlines() throws IOException {
+
+        // Create two log events containing new lines.
+        final Level level = Level.DEBUG;
+        final String loggerFqcn = "f.q.c.n";
+        final String loggerName = "A";
+        final SimpleMessage message1 = new SimpleMessage("line1\nline2\r\nline3");
+        final long instantMillis1 = Instant.EPOCH.toEpochMilli();
+        final LogEvent logEvent1 = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(loggerName)
+                .setLoggerFqcn(loggerFqcn)
+                .setLevel(level)
+                .setMessage(message1)
+                .setTimeMillis(instantMillis1)
+                .build();
+        final SimpleMessage message2 = new SimpleMessage("line4\nline5\r\nline6");
+        final long instantMillis2 = instantMillis1 + Duration.ofDays(1).toMillis();
+        final LogEvent logEvent2 = Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(loggerName)
+                .setLoggerFqcn(loggerFqcn)
+                .setLevel(level)
+                .setMessage(message2)
+                .setTimeMillis(instantMillis2)
+                .build();
+
+        try (final RestHighLevelClient client = createClient()) {
+            final Appender appender = createStartedAppender(
+                    JSON_TEMPLATE_GELF_LAYOUT,
+                    MavenHardcodedConstants.LS_GELF_INPUT_PORT);
+            try {
+
+                // Append the event.
+                LOGGER.info("appending events");
+                appender.append(logEvent1);
+                appender.append(logEvent2);
+                LOGGER.info("completed appending events");
+
+                // Wait the message to arrive.
+                Awaitility
+                        .await()
+                        .atMost(Duration.ofSeconds(60))
+                        .pollDelay(Duration.ofSeconds(2))
+                        .until(() -> queryDocumentCount(client) == 2);
+
+                // Verify indexed messages.
+                final Set<String> expectedMessages = Stream
+                        .of(logEvent1, logEvent2)
+                        .map(LogstashIT::expectedLogstashMessageField)
+                        .collect(Collectors.toSet());
+                final Set<String> actualMessages = queryDocuments(client)
+                        .stream()
+                        .map(source -> (String) source.get(ES_INDEX_MESSAGE_FIELD_NAME))
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toSet());
+                Assertions
+                        .assertThat(actualMessages)
+                        .isEqualTo(expectedMessages);
+
+            } finally {
+                appender.stop();
+            }
+        }
+
+    }
+
+    @Test
+    public void test_GelfLayout() throws IOException {
+
+        // Create log events.
+        final List<LogEvent> logEvents =
+                LogEventFixture.createFullLogEvents(LOG_EVENT_COUNT);
+
+        // Append log events and collect persisted sources.
+        final Function<Map<String, Object>, Integer> keyMapper =
+                (final Map<String, Object> source) -> {
+                    final String timestamp = (String) source.get("timestamp");
+                    final String shortMessage = (String) source.get("short_message");
+                    final String fullMessage = (String) source.get("full_message");
+                    return Objects.hash(timestamp, shortMessage, fullMessage);
+                };
+        final Map<Integer, Object> expectedSourceByKey =
+                appendAndCollect(
+                        logEvents,
+                        GELF_LAYOUT,
+                        MavenHardcodedConstants.LS_GELF_INPUT_PORT,
+                        keyMapper,
+                        Collections.emptySet());
+        final Map<Integer, Object> actualSourceByKey =
+                appendAndCollect(
+                        logEvents,
+                        JSON_TEMPLATE_GELF_LAYOUT,
+                        MavenHardcodedConstants.LS_GELF_INPUT_PORT,
+                        keyMapper,
+                        Collections.emptySet());
+
+        // Compare persisted sources.
+        Assertions.assertThat(actualSourceByKey).isEqualTo(expectedSourceByKey);
+
+    }
+
+    @Test
+    public void test_EcsLayout() throws IOException {
+
+        // Create log events.
+        final List<LogEvent> logEvents =
+                LogEventFixture.createFullLogEvents(LOG_EVENT_COUNT);
+
+        // Append log events and collect persisted sources.
+        final Function<Map<String, Object>, Integer> keyMapper =
+                (final Map<String, Object> source) -> {
+                    final String timestamp = (String) source.get("@timestamp");
+                    final String message = (String) source.get("message");
+                    final String errorMessage = (String) source.get("error.message");
+                    return Objects.hash(timestamp, message, errorMessage);
+                };
+        final Set<String> excludedKeys = Collections.singleton("port");
+        final Map<Integer, Object> expectedSourceByKey =
+                appendAndCollect(
+                        logEvents,
+                        ECS_LAYOUT,
+                        MavenHardcodedConstants.LS_TCP_INPUT_PORT,
+                        keyMapper,
+                        excludedKeys);
+        final Map<Integer, Object> actualSourceByKey =
+                appendAndCollect(
+                        logEvents,
+                        JSON_TEMPLATE_ECS_LAYOUT,
+                        MavenHardcodedConstants.LS_TCP_INPUT_PORT,
+                        keyMapper,
+                        excludedKeys);
+
+        // Compare persisted sources.
+        Assertions.assertThat(actualSourceByKey).isEqualTo(expectedSourceByKey);
+
+    }
+
+    private static <K> Map<K, Object> appendAndCollect(
+            final List<LogEvent> logEvents,
+            final Layout<?> layout,
+            final int port,
+            final Function<Map<String, Object>, K> keyMapper,
+            final Set<String> excludedKeys) throws IOException {
+        try (final RestHighLevelClient client = createClient()) {
+            final Appender appender = createStartedAppender(layout, port);
+            try {
+
+                // Append the event.
+                LOGGER.info("appending events");
+                logEvents.forEach(appender::append);
+                LOGGER.info("completed appending events");
+
+                // Wait the message to arrive.
+                Awaitility
+                        .await()
+                        .atMost(Duration.ofSeconds(60))
+                        .pollDelay(Duration.ofSeconds(2))
+                        .until(() -> queryDocumentCount(client) == LOG_EVENT_COUNT);
+
+                // Retrieve the persisted messages.
+                return queryDocuments(client)
+                        .stream()
+                        .collect(Collectors.toMap(
+                                keyMapper,
+                                (final Map<String, Object> source) -> {
+                                    excludedKeys.forEach(source::remove);
+                                    return source;
+                                }));
+
+            } finally {
+                appender.stop();
+            }
+        }
+    }
+
+    private static RestHighLevelClient createClient() throws IOException {
+
+        // Instantiate the client.
+        LOGGER.info("instantiating the ES client");
+        final HttpHost httpHost = new HttpHost(HOST_NAME, MavenHardcodedConstants.ES_PORT);
+        final RestClientBuilder clientBuilder =
+                RestClient.builder(httpHost);
+        final RestHighLevelClient client = new RestHighLevelClient(clientBuilder);
+
+        // Verify the connection.
+        LOGGER.info("verifying the ES connection");
+        final ClusterHealthResponse healthResponse = client
+                .cluster()
+                .health(new ClusterHealthRequest(), RequestOptions.DEFAULT);
+        Assertions
+                .assertThat(healthResponse.getStatus())
+                .isNotEqualTo(ClusterHealthStatus.RED);
+
+        // Delete the index.
+        LOGGER.info("deleting the ES index");
+        final DeleteIndexRequest deleteRequest =
+                new DeleteIndexRequest(MavenHardcodedConstants.ES_INDEX_NAME);
+        try {
+            final AcknowledgedResponse deleteResponse = client
+                    .indices()
+                    .delete(deleteRequest, RequestOptions.DEFAULT);
+            Assertions
+                    .assertThat(deleteResponse.isAcknowledged())
+                    .isTrue();
+        } catch (ElasticsearchStatusException error) {
+            Assertions.assertThat(error)
+                    .satisfies(ignored -> Assertions
+                            .assertThat(error.status())
+                            .isEqualTo(RestStatus.NOT_FOUND));
+        }
+
+        return client;
+
+    }
+
+    private static SocketAppender createStartedAppender(
+            final Layout<?> layout,
+            final int port) {
+        LOGGER.info("creating the appender");
+        final SocketAppender appender = SocketAppender
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setHost(HOST_NAME)
+                .setPort(port)
+                .setReconnectDelayMillis(100)
+                .setName("LogstashItAppender")
+                .setBufferedIo(false)
+                .setImmediateFail(true)
+                .setIgnoreExceptions(false)
+                .setLayout(layout)
+                .build();
+        appender.start();
+        return appender;
+    }
+
+    private static long queryDocumentCount(
+            final RestHighLevelClient client)
+            throws IOException {
+        final SearchSourceBuilder searchSourceBuilder =
+                new SearchSourceBuilder()
+                        .size(0)
+                        .fetchSource(false);
+        final SearchRequest searchRequest =
+                new SearchRequest(MavenHardcodedConstants.ES_INDEX_NAME)
+                        .source(searchSourceBuilder);
+        try {
+            final SearchResponse searchResponse =
+                    client.search(searchRequest, RequestOptions.DEFAULT);
+            return searchResponse.getHits().getTotalHits().value;
+        } catch (ElasticsearchStatusException error) {
+            if (RestStatus.NOT_FOUND.equals(error.status())) {
+                return 0L;
+            }
+            throw new IOException(error);
+        }
+    }
+
+    private static List<Map<String, Object>> queryDocuments(
+            final RestHighLevelClient client
+    ) throws IOException {
+        final SearchSourceBuilder searchSourceBuilder =
+                new SearchSourceBuilder()
+                        .size(LOG_EVENT_COUNT)
+                        .fetchSource(true);
+        final SearchRequest searchRequest =
+                new SearchRequest(MavenHardcodedConstants.ES_INDEX_NAME)
+                        .source(searchSourceBuilder);
+        try {
+            final SearchResponse searchResponse =
+                    client.search(searchRequest, RequestOptions.DEFAULT);
+            return Arrays
+                    .stream(searchResponse.getHits().getHits())
+                    .map(SearchHit::getSourceAsMap)
+                    .collect(Collectors.toList());
+        } catch (ElasticsearchStatusException error) {
+            if (RestStatus.NOT_FOUND.equals(error.status())) {
+                return Collections.emptyList();
+            }
+            throw new IOException(error);
+        }
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/JsonReaderTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/JsonReaderTest.java
new file mode 100644
index 0000000..aa4c40e
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/JsonReaderTest.java
@@ -0,0 +1,380 @@
+/*
+ * 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.assertj.core.api.Assertions;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+
+public class JsonReaderTest {
+
+    @Test
+    public void test_valid_null() {
+        test("null", null);
+        test("[null, null]", Arrays.asList(null, null));
+    }
+
+    @Test
+    public void test_invalid_null() {
+        for (final String json : new String[]{"nuL", "nulL", "nul1"}) {
+            Assertions
+                    .assertThatThrownBy(() -> JsonReader.read(json))
+                    .as("json=%s", json)
+                    .isInstanceOf(IllegalArgumentException.class)
+                    .hasMessageStartingWith("was expecting keyword 'null' at index");
+
+        }
+    }
+
+    @Test
+    public void test_valid_boolean() {
+        test("true", true);
+        test("false", false);
+        test("[true, false]", Arrays.asList(true, false));
+    }
+
+    @Test
+    public void test_invalid_boolean() {
+        for (final String json : new String[]{"tru", "truE", "fals", "falsE"}) {
+            Assertions
+                    .assertThatThrownBy(() -> JsonReader.read(json))
+                    .as("json=%s", json)
+                    .isInstanceOf(IllegalArgumentException.class)
+                    .hasMessageMatching("^was expecting keyword '(true|false)' at index [0-9]+: .*$");
+
+        }
+    }
+
+    @Test
+    public void test_valid_string() {
+        test("\"\"", "");
+        test("\" \"", " ");
+        test("\" a\"", " a");
+        test("\"a \"", "a ");
+        test("\"abc\"", "abc");
+        test("\"abc\\\"\"", "abc\"");
+        test("\"\\b\\f\\n\\r\\t\"", "\b\f\n\r\t");
+    }
+
+    @Test
+    public void test_invalid_string_start() {
+        final String json = "abc\"";
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read(json))
+                .as("json=%s", json)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("invalid character at index 0: a");
+    }
+
+    @Test
+    public void test_invalid_string_end() {
+        for (final String json : new String[]{"", " ", "\r", "\t", "\"abc"}) {
+            Assertions
+                    .assertThatThrownBy(() -> JsonReader.read(json))
+                    .as("json=%s", json)
+                    .isInstanceOf(IllegalArgumentException.class)
+                    .hasMessage("premature end of input");
+        }
+    }
+
+    @Test
+    public void test_invalid_string_escape() {
+        for (final String json : new String[]{"\"\\k\"", "\"\\d\""}) {
+            Assertions
+                    .assertThatThrownBy(() -> JsonReader.read(json))
+                    .as("json=%s", json)
+                    .isInstanceOf(IllegalArgumentException.class)
+                    .hasMessageStartingWith(
+                            "was expecting an escape character at index 2: ");
+        }
+    }
+
+    @Test
+    public void test_invalid_string_concat() {
+        final String json = "\"foo\"\"bar\"";
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read(json))
+                .as("json=%s", json)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("was not expecting input at index 5: \"");
+    }
+
+    @Test
+    public void test_valid_unicode_string() {
+        final String json = "\"a\\u00eF4bc\"";
+        Assertions
+                .assertThat(JsonReader.read(json))
+                .as("json=%s", json)
+                .isEqualTo("a\u00ef4bc");
+    }
+
+    @Test
+    public void test_invalid_unicode() {
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read("\"\\u000x\""))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("was expecting a unicode character at index 6: x");
+    }
+
+    @Test
+    public void test_valid_integers() {
+        for (final String integer : new String[]{
+                "0",
+                "1",
+                "" + Long.MAX_VALUE + "" + Long.MAX_VALUE}) {
+            for (final String signedInteger : new String[]{integer, '-' + integer}) {
+                final Object expectedToken =
+                        signedInteger.length() < 3
+                                ? Integer.parseInt(signedInteger)
+                                : new BigInteger(signedInteger);
+                test(signedInteger, expectedToken);
+            }
+        }
+    }
+
+    @Test
+    public void test_invalid_integers() {
+        for (final String integer : new String[]{
+                "0-",
+                "1a"}) {
+            for (final String signedInteger : new String[]{integer, '-' + integer}) {
+                Assertions
+                        .assertThatThrownBy(() -> JsonReader.read(signedInteger))
+                        .as("signedInteger=%s", signedInteger)
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessageStartingWith("was not expecting input at index");
+            }
+        }
+    }
+
+    @Test
+    public void test_valid_decimals() {
+        for (final String decimal : new String[]{
+                "0.0",
+                "1.0",
+                "1.2",
+                "1e2",
+                "1e-2",
+                "1.2e3",
+                "1.2e-3"}) {
+            for (final String signedDecimal : new String[]{decimal, '-' + decimal}) {
+                test(signedDecimal, new BigDecimal(signedDecimal));
+            }
+        }
+    }
+
+    @Test
+    public void test_invalid_decimals() {
+        for (final String decimal : new String[]{
+                "0.",
+                ".1",
+                "1e",
+                "1e-",
+                "1.2e",
+                "1.2e-"}) {
+            for (final String signedDecimal : new String[]{decimal, '-' + decimal}) {
+                Assertions
+                        .assertThatThrownBy(() -> JsonReader.read(signedDecimal))
+                        .as("signedDecimal=%s", signedDecimal)
+                        .isInstanceOf(IllegalArgumentException.class);
+            }
+        }
+    }
+
+    @Test
+    public void test_valid_arrays() {
+        for (final String json : new String[]{
+                "[]",
+                "[ ]"}) {
+            test(json, Collections.emptyList());
+        }
+        for (final String json : new String[]{
+                "[1]",
+                "[ 1]",
+                "[1 ]",
+                "[ 1 ]"}) {
+            test(json, Collections.singletonList(1));
+        }
+        for (final String json : new String[]{
+                "[1,2]",
+                "[1, 2]",
+                "[ 1, 2]",
+                "[1 , 2]",
+                "[ 1 , 2 ]"}) {
+            test(json, Arrays.asList(1, 2));
+        }
+    }
+
+    @Test
+    public void test_invalid_array_start() {
+        final String json = "[";
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read(json))
+                .as("json=%s", json)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("premature end of input");
+    }
+
+    @Test
+    public void test_invalid_array_end_1() {
+        final String json = "]";
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read(json))
+                .as("json=%s", json)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("was not expecting ARRAY_END at index 0");
+    }
+
+    @Test
+    public void test_invalid_array_comma() {
+        final String json = "[,";
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read(json))
+                .as("json=%s", json)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("was expecting an array element at index 1: COMMA");
+    }
+
+    @Test
+    public void test_invalid_array_end_2() {
+        final String json = "[1,";
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read(json))
+                .as("json=%s", json)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("premature end of input");
+    }
+
+    @Test
+    public void test_invalid_array_end_3() {
+        final String json = "[1,]";
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read(json))
+                .as("json=%s", json)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("was expecting an array element at index 3: ARRAY_END");
+    }
+
+    @Test
+    public void test_valid_objects() {
+        test("{}", Collections.emptyMap());
+        test("{\"foo\":\"bar\"}", Collections.singletonMap("foo", "bar"));
+    }
+
+    @Test
+    public void test_invalid_object_start() {
+        final String json = "{";
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read(json))
+                .as("json=%s", json)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("premature end of input");
+    }
+
+    @Test
+    public void test_invalid_object_end() {
+        final String json = "}";
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read(json))
+                .as("json=%s", json)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("was not expecting OBJECT_END at index 0");
+    }
+
+    @Test
+    public void test_invalid_object_colon_1() {
+        final String json = "{\"foo\"\"bar\"}";
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read(json))
+                .as("json=%s", json)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("was expecting COLON at index 6: bar");
+    }
+
+    @Test
+    public void test_invalid_object_colon_2() {
+        final String json = "{\"foo\":}";
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read(json))
+                .as("json=%s", json)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("premature end of input");
+    }
+
+    @Test
+    public void test_invalid_object_token() {
+        final String json = "{\"foo\":\"bar}";
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read(json))
+                .as("json=%s", json)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("premature end of input");
+    }
+
+    @Test
+    public void test_invalid_object_comma() {
+        final String json = "{\"foo\":\"bar\",}";
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read(json))
+                .as("json=%s", json)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("was expecting an object key at index 13: OBJECT_END");
+    }
+
+    @Test
+    public void test_invalid_object_key() {
+        final String json = "{\"foo\":\"bar\",]}";
+        Assertions
+                .assertThatThrownBy(() -> JsonReader.read(json))
+                .as("json=%s", json)
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("was expecting an object key at index 13: ARRAY_END");
+    }
+
+    @Test
+    @SuppressWarnings("DoubleBraceInitialization")
+    public void test_nesting() {
+        test(
+                "{\"k1\": [true, null, 1e5, {\"k2\": \"v2\", \"k3\": {\"k4\": \"v4\"}}]}",
+                Collections.singletonMap(
+                        "k1",
+                        Arrays.asList(
+                                true,
+                                null,
+                                new BigDecimal("1e5"),
+                                new LinkedHashMap<String, Object>() {{
+                                    put("k2", "v2");
+                                    put("k3", Collections.singletonMap("k4", "v4"));
+                                }})));
+    }
+
+    private void test(final String json, final Object expected) {
+        // Wrapping the assertion one more time to decorate it with the input.
+        Assertions
+                .assertThatCode(() -> Assertions
+                        .assertThat(JsonReader.read(json))
+                        .isEqualTo(expected))
+                .as("json=%s", json)
+                .doesNotThrowAnyException();
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/JsonWriterTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/JsonWriterTest.java
new file mode 100644
index 0000000..99451d3
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/JsonWriterTest.java
@@ -0,0 +1,729 @@
+/*
+ * 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.impl.JdkMapAdapterStringMap;
+import org.apache.logging.log4j.layout.json.template.JacksonFixture;
+import org.apache.logging.log4j.util.BiConsumer;
+import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
+import org.apache.logging.log4j.util.StringBuilderFormattable;
+import org.apache.logging.log4j.util.StringMap;
+import org.apache.logging.log4j.util.Strings;
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+@SuppressWarnings("DoubleBraceInitialization")
+public class JsonWriterTest {
+
+    private static final JsonWriter WRITER = JsonWriter
+            .newBuilder()
+            .setMaxStringLength(128)
+            .setTruncatedStringSuffix("~")
+            .build();
+
+    @Test
+    public void test_writeValue_null_Object() {
+        expectNull(() -> WRITER.writeValue(null));
+    }
+
+    @Test
+    public void test_writeValue() {
+        final Object value = Collections.singletonMap("a", "b");
+        final String expectedJson = "{'a':'b'}".replace('\'', '"');
+        final String actualJson = WRITER.use(() -> WRITER.writeValue(value));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeObject_null_StringMap() {
+        expectNull(() -> WRITER.writeObject((StringMap) null));
+    }
+
+    @Test
+    public void test_writeObject_StringMap() {
+        final StringMap map = new JdkMapAdapterStringMap(Collections.singletonMap("a", "b"));
+        final String expectedJson = "{'a':'b'}".replace('\'', '"');
+        final String actualJson = WRITER.use(() -> WRITER.writeObject(map));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeObject_null_IndexedReadOnlyStringMap() {
+        expectNull(() -> WRITER.writeObject((IndexedReadOnlyStringMap) null));
+    }
+
+    @Test
+    public void test_writeObject_IndexedReadOnlyStringMap() {
+        final IndexedReadOnlyStringMap map =
+                new SortedArrayStringMap(new LinkedHashMap<String, Object>() {{
+                    put("buzz", 1.2D);
+                    put("foo", "bar");
+                }});
+        final String expectedJson = "{'buzz':1.2,'foo':'bar'}".replace('\'', '"');
+        final String actualJson = WRITER.use(() -> WRITER.writeObject(map));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeObject_null_Map() {
+        expectNull(() -> WRITER.writeObject((Map<String, Object>) null));
+    }
+
+    @Test
+    public void test_writeObject_Map() {
+        final Map<String, Object> map = new LinkedHashMap<String, Object>() {{
+            put("key1", "val1");
+            put("key2", Collections.singletonMap("key2.1", "val2.1"));
+            put("key3", Arrays.asList(
+                    3,
+                    (byte) 127,
+                    4.5D,
+                    4.6F,
+                    Arrays.asList(true, false),
+                    new BigDecimal("30.12345678901234567890123456789"),
+                    new BigInteger("12345678901234567890123456789"),
+                    Collections.singleton('a'),
+                    Collections.singletonMap("key3.3", "val3.3")));
+            put("key4", new LinkedHashMap<String, Object>() {{
+                put("chars", new char[]{'a', 'b', 'c'});
+                put("booleans", new boolean[]{true, false});
+                put("bytes", new byte[]{1, 2});
+                put("shorts", new short[]{3, 4});
+                put("ints", new int[]{256, 257});
+                put("longs", new long[]{2147483648L, 2147483649L});
+                put("floats", new float[]{1.0F, 1.1F});
+                put("doubles", new double[]{2.0D, 2.1D});
+                put("objects", new Object[]{"foo", "bar"});
+            }});
+            put("key5\t", new Object() {
+                @Override
+                public String toString() {
+                    return "custom-object\r";
+                }
+            });
+            put("key6", Arrays.asList(
+                    new SortedArrayStringMap(new LinkedHashMap<String, Object>() {{
+                        put("buzz", 1.2D);
+                        put("foo", "bar");
+                    }}),
+                    new JdkMapAdapterStringMap(Collections.singletonMap("a", "b"))));
+            put("key7", (StringBuilderFormattable) buffer ->
+                    buffer.append(7.7777777777777D));
+        }};
+        final String expectedJson = ("{" +
+                "'key1':'val1'," +
+                "'key2':{'key2.1':'val2.1'}," +
+                "'key3':[" +
+                "3," +
+                "127," +
+                "4.5," +
+                "4.6," +
+                "[true,false]," +
+                "30.12345678901234567890123456789," +
+                "12345678901234567890123456789," +
+                "['a']," +
+                "{'key3.3':'val3.3'}" +
+                "]," +
+                "'key4':{" +
+                "'chars':['a','b','c']," +
+                "'booleans':[true,false]," +
+                "'bytes':[1,2]," +
+                "'shorts':[3,4]," +
+                "'ints':[256,257]," +
+                "'longs':[2147483648,2147483649]," +
+                "'floats':[1.0,1.1]," +
+                "'doubles':[2.0,2.1]," +
+                "'objects':['foo','bar']" +
+                "}," +
+                "'key5\\t':'custom-object\\r'," +
+                "'key6':[{'buzz':1.2,'foo':'bar'},{'a':'b'}]," +
+                "'key7':'7.7777777777777'" +
+                "}").replace('\'', '"');
+        final String actualJson = WRITER.use(() -> WRITER.writeObject(map));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeArray_null_List() {
+        expectNull(() -> WRITER.writeArray((List<Object>) null));
+    }
+
+    @Test
+    public void test_writeArray_List() {
+        final List<Object> items = Arrays.asList(
+                1, 2, 3,
+                "yo",
+                Collections.singletonMap("foo", "bar"));
+        final String expectedJson = "[1,2,3,\"yo\",{\"foo\":\"bar\"}]";
+        final String actualJson = WRITER.use(() -> WRITER.writeArray(items));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeArray_null_Collection() {
+        expectNull(() -> WRITER.writeArray((Collection<Object>) null));
+    }
+
+    @Test
+    public void test_writeArray_Collection() {
+        final Collection<Object> items = Arrays.asList(
+                1, 2, 3,
+                Collections.singletonMap("foo", "bar"));
+        final String expectedJson = "[1,2,3,{\"foo\":\"bar\"}]";
+        final String actualJson = WRITER.use(() -> WRITER.writeArray(items));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeArray_null_char() {
+        expectNull(() -> WRITER.writeArray((char[]) null));
+    }
+
+    @Test
+    public void test_writeArray_char() {
+        final char[] items = {'\u0000', 'a', 'b', 'c', '\u007f'};
+        final String expectedJson = "[\"\\u0000\",\"a\",\"b\",\"c\",\"\u007F\"]";
+        final String actualJson = WRITER.use(() -> WRITER.writeArray(items));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeArray_null_boolean() {
+        expectNull(() -> WRITER.writeArray((boolean[]) null));
+    }
+
+    @Test
+    public void test_writeArray_boolean() {
+        final boolean[] items = {true, false};
+        final String expectedJson = "[true,false]";
+        final String actualJson = WRITER.use(() -> WRITER.writeArray(items));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeArray_null_byte() {
+        expectNull(() -> WRITER.writeArray((byte[]) null));
+    }
+
+    @Test
+    public void test_writeArray_byte() {
+        final byte[] items = {Byte.MIN_VALUE, -1, 0, 1, Byte.MAX_VALUE};
+        final String expectedJson = Arrays
+                .toString(items)
+                .replaceAll(" ", "");
+        final String actualJson = WRITER.use(() -> WRITER.writeArray(items));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeArray_null_short() {
+        expectNull(() -> WRITER.writeArray((short[]) null));
+    }
+
+    @Test
+    public void test_writeArray_short() {
+        final short[] items = {Short.MIN_VALUE, -1, 0, 1, Short.MAX_VALUE};
+        final String expectedJson = Arrays
+                .toString(items)
+                .replaceAll(" ", "");
+        final String actualJson = WRITER.use(() -> WRITER.writeArray(items));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeArray_null_int() {
+        expectNull(() -> WRITER.writeArray((int[]) null));
+    }
+
+    @Test
+    public void test_writeArray_int() {
+        final int[] items = {Integer.MIN_VALUE, -1, 0, 1, Integer.MAX_VALUE};
+        final String expectedJson = Arrays
+                .toString(items)
+                .replaceAll(" ", "");
+        final String actualJson = WRITER.use(() -> WRITER.writeArray(items));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeArray_null_long() {
+        expectNull(() -> WRITER.writeArray((long[]) null));
+    }
+
+    @Test
+    public void test_writeArray_long() {
+        final long[] items = {Long.MIN_VALUE, -1L, 0L, 1L, Long.MAX_VALUE};
+        final String expectedJson = Arrays
+                .toString(items)
+                .replaceAll(" ", "");
+        final String actualJson = WRITER.use(() -> WRITER.writeArray(items));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeArray_null_float() {
+        expectNull(() -> WRITER.writeArray((float[]) null));
+    }
+
+    @Test
+    public void test_writeArray_float() {
+        final float[] items = {Float.MIN_VALUE, -1F, 0F, 1F, Float.MAX_VALUE};
+        final String expectedJson = Arrays
+                .toString(items)
+                .replaceAll(" ", "");
+        final String actualJson = WRITER.use(() -> WRITER.writeArray(items));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeArray_null_double() {
+        expectNull(() -> WRITER.writeArray((double[]) null));
+    }
+
+    @Test
+    public void test_writeArray_double() {
+        final double[] items = {Double.MIN_VALUE, -1D, 0D, 1D, Double.MAX_VALUE};
+        final String expectedJson = Arrays
+                .toString(items)
+                .replaceAll(" ", "");
+        final String actualJson = WRITER.use(() -> WRITER.writeArray(items));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeArray_null_Object() {
+        expectNull(() -> WRITER.writeArray((Object[]) null));
+    }
+
+    @Test
+    public void test_writeArray_Object() {
+        final String expectedJson = "[\"foo\",{\"bar\":\"buzz\"},null]";
+        final String actualJson = WRITER.use(() ->
+                WRITER.writeArray(new Object[]{
+                        "foo",
+                        Collections.singletonMap("bar", "buzz"),
+                        null
+                }));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeString_null_emitter() {
+        Assertions
+                .assertThatThrownBy(() ->
+                        WRITER.use(() -> WRITER.writeString(null, 0L)))
+                .isInstanceOf(NullPointerException.class)
+                .hasMessageContaining("emitter");
+    }
+
+    @Test
+    public void test_writeString_emitter() {
+        final String state = "there-is-no-spoon";
+        final BiConsumer<StringBuilder, String> emitter = StringBuilder::append;
+        final String expectedJson = '"' + state + '"';
+        final String actualJson =
+                WRITER.use(() -> WRITER.writeString(emitter, state));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeString_emitter_excessive_string() {
+        final int maxStringLength = WRITER.getMaxStringLength();
+        final String excessiveString = Strings.repeat("x", maxStringLength) + 'y';
+        final String expectedJson = '"' +
+                excessiveString.substring(0, maxStringLength) +
+                WRITER.getTruncatedStringSuffix() +
+                '"';
+        final BiConsumer<StringBuilder, String> emitter = StringBuilder::append;
+        final String actualJson =
+                WRITER.use(() -> WRITER.writeString(emitter, excessiveString));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeString_null_formattable() {
+        expectNull(() -> WRITER.writeString((StringBuilderFormattable) null));
+    }
+
+    @Test
+    public void test_writeString_formattable() {
+        final String expectedJson = "\"foo\\tbar\\tbuzz\"";
+        @SuppressWarnings("Convert2Lambda")
+        final String actualJson = WRITER.use(() ->
+                WRITER.writeString(new StringBuilderFormattable() {
+                    @Override
+                    public void formatTo(StringBuilder stringBuilder) {
+                        stringBuilder.append("foo\tbar\tbuzz");
+                    }
+                }));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeString_formattable_excessive_string() {
+        final int maxStringLength = WRITER.getMaxStringLength();
+        final String excessiveString = Strings.repeat("x", maxStringLength) + 'y';
+        final String expectedJson = '"' +
+                excessiveString.substring(0, maxStringLength) +
+                WRITER.getTruncatedStringSuffix() +
+                '"';
+        @SuppressWarnings("Convert2Lambda")
+        final String actualJson = WRITER.use(() ->
+                WRITER.writeString(new StringBuilderFormattable() {
+                    @Override
+                    public void formatTo(StringBuilder stringBuilder) {
+                        stringBuilder.append(excessiveString);
+                    }
+                }));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeString_null_seq_1() {
+        expectNull(() -> WRITER.writeString((CharSequence) null));
+    }
+
+    @Test
+    public void test_writeString_null_seq_2() {
+        expectNull(() -> WRITER.writeString((CharSequence) null, 0, 4));
+    }
+
+    @Test
+    public void test_writeString_seq_negative_offset() {
+        Assertions
+                .assertThatThrownBy(() ->
+                        WRITER.use(() -> WRITER.writeString("a", -1, 0)))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("offset");
+    }
+
+    @Test
+    public void test_writeString_seq_negative_length() {
+        Assertions
+                .assertThatThrownBy(() ->
+                        WRITER.use(() -> WRITER.writeString("a", 0, -1)))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("length");
+    }
+
+    @Test
+    public void test_writeString_excessive_seq() {
+        final CharSequence seq = Strings.repeat("x", WRITER.getMaxStringLength()) + 'y';
+        final String expectedJson = "\"" +
+                Strings.repeat("x", WRITER.getMaxStringLength()) +
+                WRITER.getTruncatedStringSuffix() +
+                '"';
+        final String actualJson = WRITER.use(() -> WRITER.writeString(seq));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeString_seq() throws IOException {
+        testQuoting((final Character c) -> {
+            final String s = "" + c;
+            return WRITER.use(() -> WRITER.writeString(s));
+        });
+    }
+
+    @Test
+    public void test_writeString_null_buffer_1() {
+        expectNull(() -> WRITER.writeString((char[]) null));
+    }
+
+    @Test
+    public void test_writeString_null_buffer_2() {
+        expectNull(() -> WRITER.writeString((char[]) null, 0, 4));
+    }
+
+    @Test
+    public void test_writeString_buffer_negative_offset() {
+        Assertions
+                .assertThatThrownBy(() ->
+                        WRITER.use(() -> WRITER.writeString(new char[]{'a'}, -1, 0)))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("offset");
+    }
+
+    @Test
+    public void test_writeString_buffer_negative_length() {
+        Assertions
+                .assertThatThrownBy(() ->
+                        WRITER.use(() -> WRITER.writeString(new char[]{'a'}, 0, -1)))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("length");
+    }
+
+    @Test
+    public void test_writeString_excessive_buffer() {
+        final char[] buffer =
+                (Strings.repeat("x", WRITER.getMaxStringLength()) + 'y')
+                        .toCharArray();
+        final String expectedJson = "\"" +
+                Strings.repeat("x", WRITER.getMaxStringLength()) +
+                WRITER.getTruncatedStringSuffix() +
+                '"';
+        final String actualJson = WRITER.use(() -> WRITER.writeString(buffer));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeString_buffer() throws IOException {
+        final char[] buffer = new char[1];
+        testQuoting((final Character c) -> {
+            buffer[0] = c;
+            return WRITER.use(() -> WRITER.writeString(buffer));
+        });
+    }
+
+    private void testQuoting(
+            final Function<Character, String> quoter) throws IOException {
+        final SoftAssertions assertions = new SoftAssertions();
+        for (char c = Character.MIN_VALUE;; c++) {
+            final String s = "" + c;
+            final String expectedJson = JacksonFixture
+                    .getObjectMapper()
+                    .writeValueAsString(s);
+            final String actualJson = quoter.apply(c);
+            assertions
+                    .assertThat(actualJson)
+                    .as("c='%c' (%d)", c, (int) c)
+                    .isEqualTo(expectedJson);
+            if (c == Character.MAX_VALUE) {
+                break;
+            }
+        }
+        assertions.assertAll();
+    }
+
+    @Test
+    public void test_writeNumber_null_BigDecimal() {
+        expectNull(() -> WRITER.writeNumber((BigDecimal) null));
+    }
+
+    @Test
+    public void test_writeNumber_BigDecimal() {
+        for (final BigDecimal number : new BigDecimal[]{
+                BigDecimal.ZERO,
+                BigDecimal.ONE,
+                BigDecimal.TEN,
+                new BigDecimal("" + Long.MAX_VALUE +
+                        "" + Long.MAX_VALUE +
+                        '.' + Long.MAX_VALUE +
+                        "" + Long.MAX_VALUE)}) {
+            final String expectedJson = String.valueOf(number);
+            final String actualJson = WRITER.use(() -> WRITER.writeNumber(number));
+            Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+        }
+    }
+
+    @Test
+    public void test_writeNumber_null_BigInteger() {
+        expectNull(() -> WRITER.writeNumber((BigInteger) null));
+    }
+
+    @Test
+    public void test_writeNumber_BigInteger() {
+        for (final BigInteger number : new BigInteger[]{
+                BigInteger.ZERO,
+                BigInteger.ONE,
+                BigInteger.TEN,
+                new BigInteger("" + Long.MAX_VALUE + "" + Long.MAX_VALUE)}) {
+            final String expectedJson = String.valueOf(number);
+            final String actualJson = WRITER.use(() -> WRITER.writeNumber(number));
+            Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+        }
+    }
+
+    @Test
+    public void test_writeNumber_float() {
+        for (final float number : new float[]{Float.MIN_VALUE, -1.0F, 0F, 1.0F, Float.MAX_VALUE}) {
+            final String expectedJson = String.valueOf(number);
+            final String actualJson = WRITER.use(() -> WRITER.writeNumber(number));
+            Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+        }
+    }
+
+    @Test
+    public void test_writeNumber_double() {
+        for (final double number : new double[]{Double.MIN_VALUE, -1.0D, 0D, 1.0D, Double.MAX_VALUE}) {
+            final String expectedJson = String.valueOf(number);
+            final String actualJson = WRITER.use(() -> WRITER.writeNumber(number));
+            Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+        }
+    }
+
+    @Test
+    public void test_writeNumber_short() {
+        for (final short number : new short[]{Short.MIN_VALUE, -1, 0, 1, Short.MAX_VALUE}) {
+            final String expectedJson = String.valueOf(number);
+            final String actualJson = WRITER.use(() -> WRITER.writeNumber(number));
+            Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+        }
+    }
+
+    @Test
+    public void test_writeNumber_int() {
+        for (final int number : new int[]{Integer.MIN_VALUE, -1, 0, 1, Integer.MAX_VALUE}) {
+            final String expectedJson = String.valueOf(number);
+            final String actualJson = WRITER.use(() -> WRITER.writeNumber(number));
+            Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+        }
+    }
+
+    @Test
+    public void test_writeNumber_long() {
+        for (final long number : new long[]{Long.MIN_VALUE, -1L, 0L, 1L, Long.MAX_VALUE}) {
+            final String expectedJson = String.valueOf(number);
+            final String actualJson = WRITER.use(() -> WRITER.writeNumber(number));
+            Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+        }
+    }
+
+    @Test
+    public void test_writeNumber_integral_and_negative_fractional() {
+        Assertions
+                .assertThatThrownBy(() -> WRITER.use(() -> WRITER.writeNumber(0, -1)))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("was expecting a positive fraction: -1");
+    }
+
+    @Test
+    public void test_writeNumber_integral_and_zero_fractional() {
+        final String expectedJson = "123";
+        final String actualJson = WRITER.use(() -> WRITER.writeNumber(123L, 0L));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeNumber_integral_and_fractional() {
+        final String expectedJson = "123.456";
+        final String actualJson = WRITER.use(() -> WRITER.writeNumber(123L, 456L));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeBoolean_true() {
+        final String expectedJson = "true";
+        final String actualJson = WRITER.use(() -> WRITER.writeBoolean(true));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeBoolean_false() {
+        final String expectedJson = "false";
+        final String actualJson = WRITER.use(() -> WRITER.writeBoolean(false));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeNull() {
+        expectNull(WRITER::writeNull);
+    }
+
+    private void expectNull(Runnable body) {
+        final String expectedJson = "null";
+        final String actualJson = WRITER.use(body);
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeRawString_null_seq() {
+        Assertions
+                .assertThatThrownBy(() ->
+                        WRITER.use(() ->
+                                WRITER.writeRawString((String) null)))
+                .isInstanceOf(NullPointerException.class)
+                .hasMessage("seq");
+    }
+
+    @Test
+    public void test_writeRawString_seq_negative_offset() {
+        Assertions
+                .assertThatThrownBy(() ->
+                        WRITER.use(() ->
+                                WRITER.writeRawString("a", -1, 0)))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("offset");
+    }
+
+    @Test
+    public void test_writeRawString_seq_negative_length() {
+        Assertions
+                .assertThatThrownBy(() ->
+                        WRITER.use(() ->
+                                WRITER.writeRawString("a", 0, -1)))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("length");
+    }
+
+    @Test
+    public void test_writeRawString_seq() {
+        final String expectedJson = "this is not a valid JSON string";
+        final String actualJson = WRITER.use(() -> WRITER.writeRawString(expectedJson));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+    @Test
+    public void test_writeRawString_null_buffer() {
+        Assertions
+                .assertThatThrownBy(() -> WRITER.use(() ->
+                        WRITER.writeRawString((char[]) null)))
+                .isInstanceOf(NullPointerException.class)
+                .hasMessage("buffer");
+    }
+
+    @Test
+    public void test_writeRawString_buffer_negative_offset() {
+        Assertions
+                .assertThatThrownBy(() ->
+                        WRITER.use(() ->
+                                WRITER.writeRawString(new char[]{'a'}, -1, 0)))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("offset");
+    }
+
+    @Test
+    public void test_writeRawString_buffer_negative_length() {
+        Assertions
+                .assertThatThrownBy(() ->
+                        WRITER.use(() ->
+                                WRITER.writeRawString(new char[]{'a'}, 0, -1)))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("length");
+    }
+
+    @Test
+    public void test_writeRawString_buffer() {
+        final String expectedJson = "this is not a valid JSON string";
+        final String actualJson = WRITER.use(() -> WRITER.writeRawString(expectedJson.toCharArray()));
+        Assertions.assertThat(actualJson).isEqualTo(expectedJson);
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactoriesTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactoriesTest.java
new file mode 100644
index 0000000..bc42d8e
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactoriesTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.plugins.convert.TypeConverter;
+import org.apache.logging.log4j.plugins.convert.TypeConverterRegistry;
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+
+import java.util.ArrayDeque;
+import java.util.concurrent.ArrayBlockingQueue;
+
+public class RecyclerFactoriesTest {
+
+    @Test
+    public void test_RecyclerFactoryConverter() throws Exception {
+
+        // Check if the type converter is registered.
+        final TypeConverter<?> converter = TypeConverterRegistry
+                .getInstance()
+                .findCompatibleConverter(RecyclerFactory.class);
+        Assertions.assertThat(converter).isNotNull();
+
+        // Check dummy recycler factory.
+        {
+            final Object actualDummyRecyclerFactory = converter.convert("dummy");
+            Assertions
+                    .assertThat(actualDummyRecyclerFactory)
+                    .isSameAs(DummyRecyclerFactory.getInstance());
+        }
+
+        // Check thread-local recycler factory.
+        {
+            final Object actualThreadLocalRecyclerFactory = converter.convert("threadLocal");
+            Assertions
+                    .assertThat(actualThreadLocalRecyclerFactory)
+                    .isSameAs(ThreadLocalRecyclerFactory.getInstance());
+        }
+
+        // Check queueing recycler factory.
+        {
+            final Object actualQueueingRecyclerFactory = converter.convert("queue");
+            Assertions
+                    .assertThat(actualQueueingRecyclerFactory)
+                    .isInstanceOf(QueueingRecyclerFactory.class);
+        }
+
+        // Check queueing recycler factory with supplier.
+        {
+            final Object recyclerFactory = converter.convert(
+                    "queue:supplier=java.util.ArrayDeque.new");
+            Assertions
+                    .assertThat(recyclerFactory)
+                    .isInstanceOf(QueueingRecyclerFactory.class);
+            final QueueingRecyclerFactory queueingRecyclerFactory =
+                    (QueueingRecyclerFactory) recyclerFactory;
+            final Recycler<Object> recycler =
+                    queueingRecyclerFactory.create(Object::new);
+            Assertions
+                    .assertThat(recycler)
+                    .isInstanceOf(QueueingRecycler.class);
+            final QueueingRecycler<Object> queueingRecycler =
+                    (QueueingRecycler<Object>) recycler;
+            Assertions
+                    .assertThat(queueingRecycler.getQueue())
+                    .isInstanceOf(ArrayDeque.class);
+        }
+
+        // Check queueing recycler factory with capacity.
+        {
+            final Object actualQueueingRecyclerFactory = converter.convert(
+                    "queue:capacity=100");
+            Assertions
+                    .assertThat(actualQueueingRecyclerFactory)
+                    .isInstanceOf(QueueingRecyclerFactory.class);
+        }
+
+        // Check queueing recycler factory with supplier and capacity.
+        {
+            final Object recyclerFactory = converter.convert(
+                    "queue:" +
+                            "supplier=java.util.concurrent.ArrayBlockingQueue.new," +
+                            "capacity=100");
+            Assertions
+                    .assertThat(recyclerFactory)
+                    .isInstanceOf(QueueingRecyclerFactory.class);
+            final QueueingRecyclerFactory queueingRecyclerFactory =
+                    (QueueingRecyclerFactory) recyclerFactory;
+            final Recycler<Object> recycler =
+                    queueingRecyclerFactory.create(Object::new);
+            Assertions
+                    .assertThat(recycler)
+                    .isInstanceOf(QueueingRecycler.class);
+            final QueueingRecycler<Object> queueingRecycler =
+                    (QueueingRecycler<Object>) recycler;
+            Assertions
+                    .assertThat(queueingRecycler.getQueue())
+                    .isInstanceOf(ArrayBlockingQueue.class);
+            final ArrayBlockingQueue<Object> queue =
+                    (ArrayBlockingQueue<Object>) queueingRecycler.getQueue();
+            Assertions.assertThat(queue.remainingCapacity()).isEqualTo(100);
+        }
+
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/StringParameterParserTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/StringParameterParserTest.java
new file mode 100644
index 0000000..5930b99
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/StringParameterParserTest.java
@@ -0,0 +1,393 @@
+package org.apache.logging.log4j.layout.json.template.util;
+
+import org.apache.logging.log4j.layout.json.template.util.StringParameterParser.DoubleQuotedStringValue;
+import org.apache.logging.log4j.layout.json.template.util.StringParameterParser.NullValue;
+import org.apache.logging.log4j.layout.json.template.util.StringParameterParser.StringValue;
+import org.apache.logging.log4j.layout.json.template.util.StringParameterParser.Value;
+import org.apache.logging.log4j.layout.json.template.util.StringParameterParser.Values;
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class StringParameterParserTest {
+
+    @Test
+    public void test_empty_string() {
+        testSuccess(
+                "",
+                Collections.emptyMap());
+    }
+
+    @Test
+    public void test_blank_string() {
+        testSuccess(
+                "\t",
+                Collections.emptyMap());
+    }
+
+    @Test
+    public void test_simple_pair() {
+        testSuccess(
+                "a=b",
+                Collections.singletonMap("a", Values.stringValue("b")));
+    }
+
+    @Test
+    public void test_simple_pair_with_whitespace_1() {
+        testSuccess(
+                " a=b",
+                Collections.singletonMap("a", Values.stringValue("b")));
+    }
+
+    @Test
+    public void test_simple_pair_with_whitespace_2() {
+        testSuccess(
+                " a =b",
+                Collections.singletonMap("a", Values.stringValue("b")));
+    }
+
+    @Test
+    public void test_simple_pair_with_whitespace_3() {
+        testSuccess(
+                " a = b",
+                Collections.singletonMap("a", Values.stringValue("b")));
+    }
+
+    @Test
+    public void test_simple_pair_with_whitespace_4() {
+        testSuccess(
+                " a = b ",
+                Collections.singletonMap("a", Values.stringValue("b")));
+    }
+
+    @Test
+    public void test_null_value_1() {
+        testSuccess(
+                "a",
+                Collections.singletonMap("a", Values.nullValue()));
+    }
+
+    @Test
+    public void test_null_value_2() {
+        testSuccess(
+                "a,b=c,d=",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.nullValue());
+                    put("b", Values.stringValue("c"));
+                    put("d", Values.nullValue());
+                }});
+    }
+
+    @Test
+    public void test_null_value_3() {
+        testSuccess(
+                "a,b=c,d",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.nullValue());
+                    put("b", Values.stringValue("c"));
+                    put("d", Values.nullValue());
+                }});
+    }
+
+    @Test
+    public void test_null_value_4() {
+        testSuccess(
+                "a,b=\"c,=\\\"\",d=,e=f",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.nullValue());
+                    put("b", Values.doubleQuotedStringValue("c,=\""));
+                    put("d", Values.nullValue());
+                    put("e", Values.stringValue("f"));
+                }});
+    }
+
+    @Test
+    public void test_two_pairs() {
+        testSuccess(
+                "a=b,c=d",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.stringValue("b"));
+                    put("c", Values.stringValue("d"));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_01() {
+        testSuccess(
+                "a=\"b\"",
+                Collections.singletonMap("a", Values.doubleQuotedStringValue("b")));
+    }
+
+    @Test
+    public void test_quoted_string_02() {
+        testSuccess(
+                "a=\"b\",c=d",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.doubleQuotedStringValue("b"));
+                    put("c", Values.stringValue("d"));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_03() {
+        testSuccess(
+                "a=b,c=\"d\"",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.stringValue("b"));
+                    put("c", Values.doubleQuotedStringValue("d"));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_04() {
+        testSuccess(
+                "a=\"b\",c=\"d\"",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.doubleQuotedStringValue("b"));
+                    put("c", Values.doubleQuotedStringValue("d"));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_05() {
+        testSuccess(
+                "a=\"\\\"b\"",
+                Collections.singletonMap("a", Values.doubleQuotedStringValue("\"b")));
+    }
+
+    @Test
+    public void test_quoted_string_06() {
+        testSuccess(
+                "a=\"\\\"b\\\"\"",
+                Collections.singletonMap("a", Values.doubleQuotedStringValue("\"b\"")));
+    }
+
+    @Test
+    public void test_quoted_string_07() {
+        testSuccess(
+                "a=\"\\\"b\",c=d",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.doubleQuotedStringValue("\"b"));
+                    put("c", Values.stringValue("d"));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_08() {
+        testSuccess(
+                "a=\"\\\"b\\\"\",c=d",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.doubleQuotedStringValue("\"b\""));
+                    put("c", Values.stringValue("d"));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_09() {
+        testSuccess(
+                "a=\"\\\"b,\",c=d",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.doubleQuotedStringValue("\"b,"));
+                    put("c", Values.stringValue("d"));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_10() {
+        testSuccess(
+                "a=\"\\\"b\\\",\",c=d",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.doubleQuotedStringValue("\"b\","));
+                    put("c", Values.stringValue("d"));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_11() {
+        testSuccess(
+                "a=\"\\\"b\",c=\"d\"",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.doubleQuotedStringValue("\"b"));
+                    put("c", Values.doubleQuotedStringValue("d"));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_12() {
+        testSuccess(
+                "a=\"\\\"b\\\"\",c=\"d\"",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.doubleQuotedStringValue("\"b\""));
+                    put("c", Values.doubleQuotedStringValue("d"));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_13() {
+        testSuccess(
+                "a=\"\\\"b,\",c=\"\\\"d\"",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.doubleQuotedStringValue("\"b,"));
+                    put("c", Values.doubleQuotedStringValue("\"d"));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_14() {
+        testSuccess(
+                "a=\"\\\"b\\\",\",c=\"\\\"d\\\"\"",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.doubleQuotedStringValue("\"b\","));
+                    put("c", Values.doubleQuotedStringValue("\"d\""));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_15() {
+        testSuccess(
+                "a=\"\\\"b\",c=\",d\"",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.doubleQuotedStringValue("\"b"));
+                    put("c", Values.doubleQuotedStringValue(",d"));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_16() {
+        testSuccess(
+                "a=\"\\\"b\\\"\",c=\",d\"",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.doubleQuotedStringValue("\"b\""));
+                    put("c", Values.doubleQuotedStringValue(",d"));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_17() {
+        testSuccess(
+                "a=\"\\\"b,\",c=\"\\\"d,\"",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.doubleQuotedStringValue("\"b,"));
+                    put("c", Values.doubleQuotedStringValue("\"d,"));
+                }});
+    }
+
+    @Test
+    public void test_quoted_string_18() {
+        testSuccess(
+                "a=\"\\\"b\\\",\",c=\"\\\"d\\\",\"",
+                new LinkedHashMap<String, Value>() {{
+                    put("a", Values.doubleQuotedStringValue("\"b\","));
+                    put("c", Values.doubleQuotedStringValue("\"d\","));
+                }});
+    }
+
+    private static void testSuccess(
+            final String input,
+            final Map<String, Value> expectedMap) {
+        final Map<String, Value> actualMap = StringParameterParser.parse(input);
+        Assertions
+                .assertThat(actualMap)
+                .as("input: %s", input)
+                .isEqualTo(expectedMap);
+    }
+
+    @Test
+    public void test_missing_key() {
+        Assertions
+                .assertThatThrownBy(() -> {
+                    final String input = ",a=b";
+                    StringParameterParser.parse(input);
+                })
+                .hasMessageStartingWith("failed to locate key at index 0");
+    }
+
+    @Test
+    public void test_conflicting_key() {
+        Assertions
+                .assertThatThrownBy(() -> {
+                    final String input = "a,a";
+                    StringParameterParser.parse(input);
+                })
+                .hasMessageStartingWith("conflicting key at index 2");
+    }
+
+    @Test
+    public void test_prematurely_ending_quoted_string_01() {
+        Assertions
+                .assertThatThrownBy(() -> {
+                    final String input = "a,b=\"";
+                    StringParameterParser.parse(input);
+                })
+                .hasMessageStartingWith("failed to locate the end of double-quoted content starting at index 4");
+    }
+
+    @Test
+    public void test_prematurely_ending_quoted_string_02() {
+        Assertions
+                .assertThatThrownBy(() -> {
+                    final String input = "a,b=\"c";
+                    StringParameterParser.parse(input);
+                })
+                .hasMessageStartingWith("failed to locate the end of double-quoted content starting at index 4");
+    }
+
+    @Test
+    public void test_prematurely_ending_quoted_string_03() {
+        Assertions
+                .assertThatThrownBy(() -> {
+                    final String input = "a,b=\",c";
+                    StringParameterParser.parse(input);
+                })
+                .hasMessageStartingWith("failed to locate the end of double-quoted content starting at index 4");
+    }
+
+    @Test
+    public void test_prematurely_ending_quoted_string_04() {
+        Assertions
+                .assertThatThrownBy(() -> {
+                    final String input = "a,b=\",c\" x";
+                    StringParameterParser.parse(input);
+                })
+                .hasMessageStartingWith("was expecting comma at index 9");
+    }
+
+    @Test
+    public void test_NullValue_toString() {
+        final Map<String, Value> map = StringParameterParser.parse("a");
+        final NullValue value = (NullValue) map.get("a");
+        Assertions.assertThat(value.toString()).isEqualTo(null);
+    }
+
+    @Test
+    public void test_StringValue_toString() {
+        final Map<String, Value> map = StringParameterParser.parse("a=b");
+        final StringValue value = (StringValue) map.get("a");
+        Assertions.assertThat(value.toString()).isEqualTo("b");
+    }
+
+    @Test
+    public void test_DoubleQuotedStringValue_toString() {
+        final Map<String, Value> map = StringParameterParser.parse("a=\"\\\"b\"");
+        final DoubleQuotedStringValue value = (DoubleQuotedStringValue) map.get("a");
+        Assertions.assertThat(value.toString()).isEqualTo("\"b");
+    }
+
+    @Test
+    public void test_allowedKeys() {
+        Assertions
+                .assertThatThrownBy(() -> {
+                    final String input = "a,b";
+                    final Set<String> allowedKeys =
+                            new LinkedHashSet<>(Collections.singletonList("a"));
+                    StringParameterParser.parse(input, allowedKeys);
+                })
+                .hasMessageStartingWith("unknown key \"b\" is found in input: a,b");
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedWriterTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedWriterTest.java
new file mode 100644
index 0000000..0373e75
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/TruncatingBufferedWriterTest.java
@@ -0,0 +1,228 @@
+package org.apache.logging.log4j.layout.json.template.util;
+
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+
+public class TruncatingBufferedWriterTest {
+
+    @Test
+    public void test_ctor_invalid_args() {
+        Assertions
+                .assertThatThrownBy(() -> new TruncatingBufferedWriter(-1))
+                .isInstanceOf(NegativeArraySizeException.class);
+    }
+
+    @Test
+    public void test_okay_payloads() {
+
+        // Fill in the writer.
+        final int capacity = 1_000;
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(capacity);
+        writer.write(Character.MAX_VALUE);
+        writer.write(new char[]{Character.MIN_VALUE, Character.MAX_VALUE});
+        writer.write("foo");
+        writer.write("foobar", 3, 3);
+        writer.write(new char[]{'f', 'o', 'o', 'b', 'a', 'r', 'b', 'u', 'z', 'z'}, 6, 4);
+        writer.append('!');
+        writer.append("yo");
+        writer.append(null);
+        writer.append("yo dog", 3, 6);
+        writer.append(null, -1, -1);
+
+        // Verify accessors.
+        final char[] expectedBuffer = new char[capacity];
+        int expectedPosition = 0;
+        expectedBuffer[expectedPosition++] = Character.MAX_VALUE;
+        expectedBuffer[expectedPosition++] = Character.MIN_VALUE;
+        expectedBuffer[expectedPosition++] = Character.MAX_VALUE;
+        expectedBuffer[expectedPosition++] = 'f';
+        expectedBuffer[expectedPosition++] = 'o';
+        expectedBuffer[expectedPosition++] = 'o';
+        expectedBuffer[expectedPosition++] = 'b';
+        expectedBuffer[expectedPosition++] = 'a';
+        expectedBuffer[expectedPosition++] = 'r';
+        expectedBuffer[expectedPosition++] = 'b';
+        expectedBuffer[expectedPosition++] = 'u';
+        expectedBuffer[expectedPosition++] = 'z';
+        expectedBuffer[expectedPosition++] = 'z';
+        expectedBuffer[expectedPosition++] = '!';
+        expectedBuffer[expectedPosition++] = 'y';
+        expectedBuffer[expectedPosition++] = 'o';
+        expectedBuffer[expectedPosition++] = 'n';
+        expectedBuffer[expectedPosition++] = 'u';
+        expectedBuffer[expectedPosition++] = 'l';
+        expectedBuffer[expectedPosition++] = 'l';
+        expectedBuffer[expectedPosition++] = 'd';
+        expectedBuffer[expectedPosition++] = 'o';
+        expectedBuffer[expectedPosition++] = 'g';
+        expectedBuffer[expectedPosition++] = 'n';
+        expectedBuffer[expectedPosition++] = 'u';
+        expectedBuffer[expectedPosition++] = 'l';
+        expectedBuffer[expectedPosition++] = 'l';
+        Assertions.assertThat(writer.getBuffer()).isEqualTo(expectedBuffer);
+        Assertions.assertThat(writer.getPosition()).isEqualTo(expectedPosition);
+        Assertions.assertThat(writer.getCapacity()).isEqualTo(capacity);
+        Assertions.assertThat(writer.isTruncated()).isFalse();
+        verifyClose(writer);
+
+    }
+
+    @Test
+    public void test_write_int_truncation() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(1);
+        writer.write('a');
+        writer.write('b');
+        verifyTruncation(writer, 'a');
+    }
+
+    @Test
+    public void test_write_char_array_truncation() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(1);
+        writer.write(new char[]{'a', 'b'});
+        verifyTruncation(writer, 'a');
+    }
+
+    @Test
+    public void test_write_String_truncation() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(1);
+        writer.write("ab");
+        verifyTruncation(writer, 'a');
+    }
+
+    @Test
+    public void test_write_String_slice_invalid_args() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(1);
+        final String string = "a";
+        Assertions
+                .assertThatThrownBy(() -> writer.write(string, -1, 1))
+                .isInstanceOf(IndexOutOfBoundsException.class)
+                .hasMessageStartingWith("invalid offset");
+        Assertions
+                .assertThatThrownBy(() -> writer.write(string, 1, 1))
+                .isInstanceOf(IndexOutOfBoundsException.class)
+                .hasMessageStartingWith("invalid offset");
+        Assertions
+                .assertThatThrownBy(() -> writer.write(string, 0, -1))
+                .isInstanceOf(IndexOutOfBoundsException.class)
+                .hasMessageStartingWith("invalid length");
+        Assertions
+                .assertThatThrownBy(() -> writer.write(string, 0, 2))
+                .isInstanceOf(IndexOutOfBoundsException.class)
+                .hasMessageStartingWith("invalid length");
+    }
+
+    @Test
+    public void test_write_String_slice_truncation() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(1);
+        writer.write("ab", 0, 2);
+        verifyTruncation(writer, 'a');
+    }
+
+    @Test
+    public void test_write_char_array_slice_invalid_args() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(1);
+        final char[] buffer = new char[]{'a'};
+        Assertions
+                .assertThatThrownBy(() -> writer.write(buffer, -1, 1))
+                .isInstanceOf(IndexOutOfBoundsException.class)
+                .hasMessageStartingWith("invalid offset");
+        Assertions
+                .assertThatThrownBy(() -> writer.write(buffer, 1, 1))
+                .isInstanceOf(IndexOutOfBoundsException.class)
+                .hasMessageStartingWith("invalid offset");
+        Assertions
+                .assertThatThrownBy(() -> writer.write(buffer, 0, -1))
+                .isInstanceOf(IndexOutOfBoundsException.class)
+                .hasMessageStartingWith("invalid length");
+        Assertions
+                .assertThatThrownBy(() -> writer.write(buffer, 0, 2))
+                .isInstanceOf(IndexOutOfBoundsException.class)
+                .hasMessageStartingWith("invalid length");
+    }
+
+    @Test
+    public void test_write_char_array_slice_truncation() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(1);
+        writer.write(new char[]{'a', 'b'}, 0, 2);
+        verifyTruncation(writer, 'a');
+    }
+
+    @Test
+    public void test_append_char_truncation() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(1);
+        writer.append('a');
+        writer.append('b');
+        verifyTruncation(writer, 'a');
+    }
+
+    @Test
+    public void test_append_seq_truncation() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(1);
+        writer.append("ab");
+        verifyTruncation(writer, 'a');
+    }
+
+    @Test
+    public void test_append_seq_null_truncation() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(1);
+        writer.append(null);
+        verifyTruncation(writer, 'n');
+    }
+
+    @Test
+    public void test_append_seq_slice_invalid_args() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(1);
+        final CharSequence seq = "ab";
+        Assertions
+                .assertThatThrownBy(() -> writer.append(seq, -1, 2))
+                .isInstanceOf(IndexOutOfBoundsException.class)
+                .hasMessageStartingWith("invalid start");
+        Assertions
+                .assertThatThrownBy(() -> writer.append(seq, 2, 2))
+                .isInstanceOf(IndexOutOfBoundsException.class)
+                .hasMessageStartingWith("invalid start");
+        Assertions
+                .assertThatThrownBy(() -> writer.append(seq, 0, -1))
+                .isInstanceOf(IndexOutOfBoundsException.class)
+                .hasMessageStartingWith("invalid end");
+        Assertions
+                .assertThatThrownBy(() -> writer.append(seq, 1, 0))
+                .isInstanceOf(IndexOutOfBoundsException.class)
+                .hasMessageStartingWith("invalid end");
+        Assertions
+                .assertThatThrownBy(() -> writer.append(seq, 0, 3))
+                .isInstanceOf(IndexOutOfBoundsException.class)
+                .hasMessageStartingWith("invalid end");
+    }
+
+    @Test
+    public void test_append_seq_slice_truncation() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(1);
+        writer.append("ab", 0, 1);
+        verifyTruncation(writer, 'a');
+    }
+
+    @Test
+    public void test_append_seq_slice_null_truncation() {
+        final TruncatingBufferedWriter writer = new TruncatingBufferedWriter(1);
+        writer.append(null, -1, -1);
+        verifyTruncation(writer, 'n');
+    }
+
+    private void verifyTruncation(
+            final TruncatingBufferedWriter writer,
+            final char c) {
+        Assertions.assertThat(writer.getBuffer()).isEqualTo(new char[]{c});
+        Assertions.assertThat(writer.getPosition()).isEqualTo(1);
+        Assertions.assertThat(writer.getCapacity()).isEqualTo(1);
+        Assertions.assertThat(writer.isTruncated()).isTrue();
+        verifyClose(writer);
+    }
+
+    private void verifyClose(final TruncatingBufferedWriter writer) {
+        writer.close();
+        Assertions.assertThat(writer.getPosition()).isEqualTo(0);
+        Assertions.assertThat(writer.isTruncated()).isFalse();
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/UrisTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/UrisTest.java
new file mode 100644
index 0000000..05e679b
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/util/UrisTest.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.util;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+public class UrisTest {
+
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
+    @Test
+    public void testClassPathResource() {
+        final String content = Uris.readUri(
+                "classpath:JsonLayout.json",
+                StandardCharsets.US_ASCII);
+        Assert.assertTrue(
+                "was expecting content to start with '{': " + content,
+                content.startsWith("{"));
+    }
+
+    @Test
+    public void testFilePathResource() throws IOException {
+        final String nonAsciiUtfText = "அஆஇฬ๘";
+        final File file = Files.createTempFile("log4j-UriUtilTest-", ".txt").toFile();
+        try {
+            try (final OutputStream outputStream = new FileOutputStream(file)) {
+                outputStream.write(nonAsciiUtfText.getBytes(StandardCharsets.UTF_8));
+            }
+            final URI uri = file.toURI();
+            final String content = Uris.readUri(uri, StandardCharsets.UTF_8);
+            Assert.assertEquals(nonAsciiUtfText, content);
+        } finally {
+            final boolean deleted = file.delete();
+            if (!deleted) {
+                LOGGER.warn("could not delete temporary file: " + file);
+            }
+        }
+    }
+
+}
diff --git a/log4j-layout-json-template/src/test/resources/gcFreeJsonTemplateLayoutLogging.xml b/log4j-layout-json-template/src/test/resources/gcFreeJsonTemplateLayoutLogging.xml
new file mode 100644
index 0000000..245ff06
--- /dev/null
+++ b/log4j-layout-json-template/src/test/resources/gcFreeJsonTemplateLayoutLogging.xml
@@ -0,0 +1,39 @@
+<?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.
+
+-->
+<Configuration status="OFF">
+    <Appenders>
+        <Console name="Console" target="SYSTEM_OUT">
+            <PatternLayout pattern="%p %c{1.} [%t] %X{aKey} %X %m%ex%n" />
+        </Console>
+        <File name="File"
+              fileName="target/gcFreeJsonTemplateLayoutLogging.log"
+              bufferedIO="false"
+              append="false">
+            <JsonTemplateLayout recyclerFactory="threadLocal"/>
+        </File>
+    </Appenders>
+    <Loggers>
+        <Root level="trace" includeLocation="false">
+            <Property name="prop1">value1</Property>
+            <Property name="prop2">value2</Property>
+            <appender-ref ref="Console" level="FATAL"/>
+            <appender-ref ref="File"/>
+        </Root>
+    </Loggers>
+</Configuration>
diff --git a/log4j-layout-json-template/src/test/resources/nullEventDelimitedJsonTemplateLayoutLogging.xml b/log4j-layout-json-template/src/test/resources/nullEventDelimitedJsonTemplateLayoutLogging.xml
new file mode 100644
index 0000000..39d87a9
--- /dev/null
+++ b/log4j-layout-json-template/src/test/resources/nullEventDelimitedJsonTemplateLayoutLogging.xml
@@ -0,0 +1,39 @@
+<?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.
+
+-->
+<Configuration status="OFF">
+    <Appenders>
+        <Socket name="Socket"
+                host="localhost"
+                port="50514"
+                protocol="TCP"
+                ignoreExceptions="false"
+                reconnectionDelay="100"
+                immediateFlush="true">
+            <JsonTemplateLayout eventTemplate='{"$resolver": "message"}'
+                                eventDelimiter=""
+                                nullEventDelimiterEnabled="true"
+                                charset="US-ASCII"/>
+        </Socket>
+    </Appenders>
+    <Loggers>
+        <Root level="TRACE">
+            <AppenderRef ref="Socket"/>
+        </Root>
+    </Loggers>
+</Configuration>
diff --git a/log4j-layout-json-template/src/test/resources/testJsonTemplateLayout.json b/log4j-layout-json-template/src/test/resources/testJsonTemplateLayout.json
new file mode 100644
index 0000000..daf455e
--- /dev/null
+++ b/log4j-layout-json-template/src/test/resources/testJsonTemplateLayout.json
@@ -0,0 +1,68 @@
+{
+  "exception_class": {
+    "$resolver": "exception",
+    "field": "className"
+  },
+  "exception_message": {
+    "$resolver": "exception",
+    "field": "message"
+  },
+  "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_id": {
+    "$resolver": "thread",
+    "field": "id"
+  },
+  "thread_name": {
+    "$resolver": "thread",
+    "field": "name"
+  },
+  "thread_priority": {
+    "$resolver": "thread",
+    "field": "priority"
+  },
+  "@timestamp": {
+    "$resolver": "timestamp"
+  },
+  "level": {
+    "$resolver": "level",
+    "field": "name"
+  },
+  "file": {
+    "$resolver": "source",
+    "field": "fileName"
+  },
+  "method": {
+    "$resolver": "source",
+    "field": "methodName"
+  },
+  "logger_fqcn": {
+    "$resolver": "logger",
+    "field": "fqcn"
+  },
+  "logger_name": {
+    "$resolver": "logger",
+    "field": "name"
+  },
+  "end_of_batch": {
+    "$resolver": "endOfBatch"
+  },
+  "lookup_test_key": "${sys:lookup_test_key}"
+}
diff --git a/log4j-mongodb2/revapi.json b/log4j-mongodb2/revapi.json
deleted file mode 100644
index bd8fb67..0000000
--- a/log4j-mongodb2/revapi.json
+++ /dev/null
@@ -1,23 +0,0 @@
-[
-  {
-    "extension": "revapi.java",
-    "configuration": {
-      "filter": {
-        "classes": {
-          "exclude": [
-          ]
-        }
-      }
-    }
-  },
-  {
-    "extension": "revapi.ignore",
-    "configuration": [
-      {
-        "code": "java.method.removed",
-        "old": "method org.apache.logging.log4j.mongodb2.MongoDbProvider org.apache.logging.log4j.mongodb2.MongoDbProvider::createNoSqlProvider(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)",
-        "justification": "LOG4J2-2493 - Remove deprecated code"
-      }
-    ]
-  }
-]
\ No newline at end of file
diff --git a/log4j-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/MongoDbConnection.java b/log4j-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/MongoDbConnection.java
deleted file mode 100644
index ec3b7dd..0000000
--- a/log4j-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/MongoDbConnection.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-package org.apache.logging.log4j.mongodb2;
-
-import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.core.appender.AppenderLoggingException;
-import org.apache.logging.log4j.core.appender.nosql.AbstractNoSqlConnection;
-import org.apache.logging.log4j.core.appender.nosql.NoSqlConnection;
-import org.apache.logging.log4j.core.appender.nosql.NoSqlObject;
-import org.apache.logging.log4j.status.StatusLogger;
-import org.bson.BSON;
-import org.bson.Transformer;
-
-import com.mongodb.BasicDBObject;
-import com.mongodb.DB;
-import com.mongodb.DBCollection;
-import com.mongodb.Mongo;
-import com.mongodb.MongoException;
-import com.mongodb.WriteConcern;
-
-/**
- * The MongoDB implementation of {@link NoSqlConnection}.
- */
-public final class MongoDbConnection extends AbstractNoSqlConnection<BasicDBObject, MongoDbObject> {
-
-    private static final Logger LOGGER = StatusLogger.getLogger();
-
-    static {
-        BSON.addEncodingHook(Level.class, new Transformer() {
-            @Override
-            public Object transform(final Object o) {
-                if (o instanceof Level) {
-                    return ((Level) o).name();
-                }
-                return o;
-            }
-        });
-    }
-
-    private final DBCollection collection;
-    private final WriteConcern writeConcern;
-
-    public MongoDbConnection(final DB database, final WriteConcern writeConcern, final String collectionName,
-            final Boolean isCapped, final Integer collectionSize) {
-        if (database.collectionExists(collectionName)) {
-            LOGGER.debug("Gettting collection {}", collectionName);
-            collection = database.getCollection(collectionName);
-        } else {
-            final BasicDBObject options = new BasicDBObject();
-            options.put("capped", isCapped);
-            options.put("size", collectionSize);
-            LOGGER.debug("Creating collection {} (capped = {}, size = {})", collectionName, isCapped, collectionSize);
-            this.collection = database.createCollection(collectionName, options);
-        }
-        this.writeConcern = writeConcern;
-    }
-
-    @Override
-    public void closeImpl() {
-        // LOG4J2-1196
-        final Mongo mongo = this.collection.getDB().getMongo();
-        LOGGER.debug("Closing {} client {}", mongo.getClass().getSimpleName(), mongo);
-        mongo.close();
-    }
-
-    @Override
-    public MongoDbObject[] createList(final int length) {
-        return new MongoDbObject[length];
-    }
-
-    @Override
-    public MongoDbObject createObject() {
-        return new MongoDbObject();
-    }
-
-    @Override
-    public void insertObject(final NoSqlObject<BasicDBObject> object) {
-        try {
-            final BasicDBObject unwrapped = object.unwrap();
-            LOGGER.debug("Inserting object {}", unwrapped);
-            this.collection.insert(unwrapped, this.writeConcern);
-        } catch (final MongoException e) {
-            throw new AppenderLoggingException("Failed to write log event to MongoDB due to error: " + e.getMessage(),
-                    e);
-        }
-    }
-
-}
diff --git a/log4j-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/MongoDbObject.java b/log4j-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/MongoDbObject.java
deleted file mode 100644
index 28a35cd..0000000
--- a/log4j-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/MongoDbObject.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-package org.apache.logging.log4j.mongodb2;
-
-import java.util.Collections;
-
-import org.apache.logging.log4j.core.appender.nosql.NoSqlObject;
-
-import com.mongodb.BasicDBList;
-import com.mongodb.BasicDBObject;
-
-/**
- * The MongoDB implementation of {@link NoSqlObject}.
- */
-public final class MongoDbObject implements NoSqlObject<BasicDBObject> {
-    private final BasicDBObject mongoObject;
-
-    public MongoDbObject() {
-        this.mongoObject = new BasicDBObject();
-    }
-
-    @Override
-    public void set(final String field, final NoSqlObject<BasicDBObject> value) {
-        this.mongoObject.append(field, value.unwrap());
-    }
-
-    @Override
-    public void set(final String field, final NoSqlObject<BasicDBObject>[] values) {
-        final BasicDBList list = new BasicDBList();
-        for (final NoSqlObject<BasicDBObject> value : values) {
-            list.add(value.unwrap());
-        }
-        this.mongoObject.append(field, list);
-    }
-
-    @Override
-    public void set(final String field, final Object value) {
-        this.mongoObject.append(field, value);
-    }
-
-    @Override
-    public void set(final String field, final Object[] values) {
-        final BasicDBList list = new BasicDBList();
-        Collections.addAll(list, values);
-        this.mongoObject.append(field, list);
-    }
-
-    @Override
-    public BasicDBObject unwrap() {
-        return this.mongoObject;
-    }
-}
diff --git a/log4j-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/MongoDbProvider.java b/log4j-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/MongoDbProvider.java
deleted file mode 100644
index e38f8d1..0000000
--- a/log4j-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/MongoDbProvider.java
+++ /dev/null
@@ -1,310 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-package org.apache.logging.log4j.mongodb2;
-
-import com.mongodb.DB;
-import com.mongodb.MongoClient;
-import com.mongodb.MongoCredential;
-import com.mongodb.ServerAddress;
-import com.mongodb.WriteConcern;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.core.Core;
-import org.apache.logging.log4j.core.appender.nosql.NoSqlProvider;
-import org.apache.logging.log4j.core.filter.AbstractFilterable;
-import org.apache.logging.log4j.plugins.Plugin;
-import org.apache.logging.log4j.plugins.PluginAliases;
-import org.apache.logging.log4j.plugins.PluginBuilderAttribute;
-import org.apache.logging.log4j.plugins.PluginFactory;
-import org.apache.logging.log4j.plugins.convert.TypeConverters;
-import org.apache.logging.log4j.plugins.validation.constraints.Required;
-import org.apache.logging.log4j.plugins.validation.constraints.ValidHost;
-import org.apache.logging.log4j.plugins.validation.constraints.ValidPort;
-import org.apache.logging.log4j.status.StatusLogger;
-import org.apache.logging.log4j.util.LoaderUtil;
-import org.apache.logging.log4j.util.NameUtil;
-import org.apache.logging.log4j.util.Strings;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * The MongoDB implementation of {@link NoSqlProvider}.
- */
-@Plugin(name = "MongoDb2", category = Core.CATEGORY_NAME, printObject = true)
-@PluginAliases("MongoDb") // Deprecated alias
-public final class MongoDbProvider implements NoSqlProvider<MongoDbConnection> {
-
-    public static class Builder<B extends Builder<B>> extends AbstractFilterable.Builder<B>
-			implements org.apache.logging.log4j.plugins.util.Builder<MongoDbProvider> {
-
-		private static WriteConcern toWriteConcern(final String writeConcernConstant,
-	            final String writeConcernConstantClassName) {
-	        WriteConcern writeConcern;
-	        if (Strings.isNotEmpty(writeConcernConstant)) {
-	            if (Strings.isNotEmpty(writeConcernConstantClassName)) {
-	                try {
-	                    final Class<?> writeConcernConstantClass = LoaderUtil.loadClass(writeConcernConstantClassName);
-	                    final Field field = writeConcernConstantClass.getField(writeConcernConstant);
-	                    writeConcern = (WriteConcern) field.get(null);
-	                } catch (final Exception e) {
-	                    LOGGER.error("Write concern constant [{}.{}] not found, using default.",
-	                            writeConcernConstantClassName, writeConcernConstant);
-	                    writeConcern = DEFAULT_WRITE_CONCERN;
-	                }
-	            } else {
-	                writeConcern = WriteConcern.valueOf(writeConcernConstant);
-	                if (writeConcern == null) {
-	                    LOGGER.warn("Write concern constant [{}] not found, using default.", writeConcernConstant);
-	                    writeConcern = DEFAULT_WRITE_CONCERN;
-	                }
-	            }
-	        } else {
-	            writeConcern = DEFAULT_WRITE_CONCERN;
-	        }
-	        return writeConcern;
-	    }
-
-		@PluginBuilderAttribute
-		@ValidHost
-		private String server = "localhost";
-
-		@PluginBuilderAttribute
-		@ValidPort
-		private String port = "" + DEFAULT_PORT;
-
-		@PluginBuilderAttribute
-		@Required(message = "No database name provided")
-		private String databaseName;
-
-		@PluginBuilderAttribute
-		@Required(message = "No collection name provided")
-		private String collectionName;
-
-		@PluginBuilderAttribute
-		private String userName;
-
-		@PluginBuilderAttribute(sensitive = true)
-		private String password;
-
-		@PluginBuilderAttribute("capped")
-		private boolean isCapped = false;
-
-		@PluginBuilderAttribute
-		private int collectionSize = DEFAULT_COLLECTION_SIZE;
-
-		@PluginBuilderAttribute
-		private String factoryClassName;
-
-		@PluginBuilderAttribute
-		private String factoryMethodName;
-
-		@PluginBuilderAttribute
-		private String writeConcernConstantClassName;
-
-		@PluginBuilderAttribute
-		private String writeConcernConstant;
-
-		@Override
-		public MongoDbProvider build() {
-	        DB database;
-	        String description;
-	        if (Strings.isNotEmpty(factoryClassName) && Strings.isNotEmpty(factoryMethodName)) {
-	            try {
-	                final Class<?> factoryClass = LoaderUtil.loadClass(factoryClassName);
-	                final Method method = factoryClass.getMethod(factoryMethodName);
-	                final Object object = method.invoke(null);
-
-	                if (object instanceof DB) {
-	                    database = (DB) object;
-	                } else if (object instanceof MongoClient) {
-	                    if (Strings.isNotEmpty(databaseName)) {
-	                        database = ((MongoClient) object).getDB(databaseName);
-	                    } else {
-	                        LOGGER.error("The factory method [{}.{}()] returned a MongoClient so the database name is "
-	                                + "required.", factoryClassName, factoryMethodName);
-	                        return null;
-	                    }
-	                } else if (object == null) {
-	                    LOGGER.error("The factory method [{}.{}()] returned null.", factoryClassName, factoryMethodName);
-	                    return null;
-	                } else {
-	                    LOGGER.error("The factory method [{}.{}()] returned an unsupported type [{}].", factoryClassName,
-	                            factoryMethodName, object.getClass().getName());
-	                    return null;
-	                }
-
-	                description = "database=" + database.getName();
-	                final List<ServerAddress> addresses = database.getMongo().getAllAddress();
-	                if (addresses.size() == 1) {
-	                    description += ", server=" + addresses.get(0).getHost() + ", port=" + addresses.get(0).getPort();
-	                } else {
-	                    description += ", servers=[";
-	                    for (final ServerAddress address : addresses) {
-	                        description += " { " + address.getHost() + ", " + address.getPort() + " } ";
-	                    }
-	                    description += "]";
-	                }
-	            } catch (final ClassNotFoundException e) {
-	                LOGGER.error("The factory class [{}] could not be loaded.", factoryClassName, e);
-	                return null;
-	            } catch (final NoSuchMethodException e) {
-	                LOGGER.error("The factory class [{}] does not have a no-arg method named [{}].", factoryClassName,
-	                        factoryMethodName, e);
-	                return null;
-	            } catch (final Exception e) {
-	                LOGGER.error("The factory method [{}.{}()] could not be invoked.", factoryClassName, factoryMethodName,
-	                        e);
-	                return null;
-	            }
-	        } else if (Strings.isNotEmpty(databaseName)) {
-	            final List<MongoCredential> credentials = new ArrayList<>();
-	            description = "database=" + databaseName;
-	            if (Strings.isNotEmpty(userName) && Strings.isNotEmpty(password)) {
-	                description += ", username=" + userName + ", passwordHash="
-	                        + NameUtil.md5(password + MongoDbProvider.class.getName());
-	                credentials.add(MongoCredential.createCredential(userName, databaseName, password.toCharArray()));
-	            }
-	            try {
-	                final int portInt = TypeConverters.convert(port, int.class, DEFAULT_PORT);
-	                description += ", server=" + server + ", port=" + portInt;
-	                database = new MongoClient(new ServerAddress(server, portInt), credentials).getDB(databaseName);
-	            } catch (final Exception e) {
-	                LOGGER.error(
-	                        "Failed to obtain a database instance from the MongoClient at server [{}] and " + "port [{}].",
-	                        server, port);
-	                return null;
-	            }
-	        } else {
-	            LOGGER.error("No factory method was provided so the database name is required.");
-	            return null;
-	        }
-
-	        try {
-	            database.getCollectionNames(); // Check if the database actually requires authentication
-	        } catch (final Exception e) {
-	            LOGGER.error(
-	                    "The database is not up, or you are not authenticated, try supplying a username and password to the MongoDB provider.",
-	                    e);
-	            return null;
-	        }
-
-	        final WriteConcern writeConcern = toWriteConcern(writeConcernConstant, writeConcernConstantClassName);
-
-	        return new MongoDbProvider(database, writeConcern, collectionName, isCapped, collectionSize, description);
-		}
-
-		public B setCapped(final boolean isCapped) {
-			this.isCapped = isCapped;
-			return asBuilder();
-		}
-
-		public B setCollectionName(final String collectionName) {
-			this.collectionName = collectionName;
-			return asBuilder();
-		}
-
-		public B setCollectionSize(final int collectionSize) {
-			this.collectionSize = collectionSize;
-			return asBuilder();
-		}
-
-		public B setDatabaseName(final String databaseName) {
-			this.databaseName = databaseName;
-			return asBuilder();
-		}
-
-		public B setFactoryClassName(final String factoryClassName) {
-			this.factoryClassName = factoryClassName;
-			return asBuilder();
-		}
-
-		public B setFactoryMethodName(final String factoryMethodName) {
-			this.factoryMethodName = factoryMethodName;
-			return asBuilder();
-		}
-
-		public B setPassword(final String password) {
-			this.password = password;
-			return asBuilder();
-		}
-
-		public B setPort(final String port) {
-			this.port = port;
-			return asBuilder();
-		}
-
-		public B setServer(final String server) {
-			this.server = server;
-			return asBuilder();
-		}
-
-		public B setUserName(final String userName) {
-			this.userName = userName;
-			return asBuilder();
-		}
-
-		public B setWriteConcernConstant(final String writeConcernConstant) {
-			this.writeConcernConstant = writeConcernConstant;
-			return asBuilder();
-		}
-
-	    public B setWriteConcernConstantClassName(final String writeConcernConstantClassName) {
-			this.writeConcernConstantClassName = writeConcernConstantClassName;
-			return asBuilder();
-		}
-    }
-    private static final WriteConcern DEFAULT_WRITE_CONCERN = WriteConcern.ACKNOWLEDGED;
-    private static final Logger LOGGER = StatusLogger.getLogger();
-    private static final int DEFAULT_PORT = 27017;
-
-    private static final int DEFAULT_COLLECTION_SIZE = 536870912;
-    @PluginFactory
-	public static <B extends Builder<B>> B newBuilder() {
-		return new Builder<B>().asBuilder();
-	}
-    private final String collectionName;
-    private final DB database;
-    private final String description;
-
-    private final WriteConcern writeConcern;
-
-    private final boolean isCapped;
-
-    private final Integer collectionSize;
-
-    private MongoDbProvider(final DB database, final WriteConcern writeConcern, final String collectionName,
-            final boolean isCapped, final Integer collectionSize, final String description) {
-        this.database = database;
-        this.writeConcern = writeConcern;
-        this.collectionName = collectionName;
-        this.isCapped = isCapped;
-        this.collectionSize = collectionSize;
-        this.description = "mongoDb{ " + description + " }";
-    }
-
-	@Override
-    public MongoDbConnection getConnection() {
-        return new MongoDbConnection(this.database, this.writeConcern, this.collectionName, this.isCapped, this.collectionSize);
-    }
-
-	@Override
-    public String toString() {
-        return this.description;
-    }
-}
diff --git a/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbAuthFailureTest.java b/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbAuthFailureTest.java
deleted file mode 100644
index 0fb61c6..0000000
--- a/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbAuthFailureTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-package org.apache.logging.log4j.mongodb2;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.categories.Appenders;
-import org.apache.logging.log4j.junit.LoggerContextRule;
-import org.apache.logging.log4j.mongodb2.MongoDbTestRule.LoggingTarget;
-import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
-import org.apache.logging.log4j.test.RuleChainFactory;
-import org.junit.Assert;
-import org.junit.ClassRule;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.RuleChain;
-
-import com.mongodb.DB;
-import com.mongodb.DBCollection;
-import com.mongodb.MongoClient;
-
-/**
- * This class name does NOT end in "Test" in order to only be picked up by {@link Java8Test}.
- *
- * TODO Set up the log4j user in MongoDB.
- */
-@Ignore("TODO Set up the log4j user in MongoDB")
-@Category(Appenders.MongoDb.class)
-public class MongoDbAuthFailureTest {
-
-    private static LoggerContextRule loggerContextTestRule = new LoggerContextRule("log4j2-mongodb-auth-failure.xml");
-
-    private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
-
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(), LoggingTarget.NULL);
-
-    @ClassRule
-    public static RuleChain ruleChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule,
-            loggerContextTestRule);
-
-    @Test
-    public void test() {
-        final Logger logger = LogManager.getLogger();
-        logger.info("Hello log");
-        final MongoClient mongoClient = mongoDbTestRule.getMongoClient();
-        try {
-            final DB database = mongoClient.getDB("test");
-            Assert.assertNotNull(database);
-            final DBCollection collection = database.getCollection("applog");
-            Assert.assertNotNull(collection);
-            Assert.assertFalse(collection.find().hasNext());
-        } finally {
-            mongoClient.close();
-        }
-    }
-}
diff --git a/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbCappedTest.java b/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbCappedTest.java
deleted file mode 100644
index ffd368a..0000000
--- a/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbCappedTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-package org.apache.logging.log4j.mongodb2;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.categories.Appenders;
-import org.apache.logging.log4j.junit.LoggerContextRule;
-import org.apache.logging.log4j.mongodb2.MongoDbTestRule.LoggingTarget;
-import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
-import org.apache.logging.log4j.test.RuleChainFactory;
-import org.junit.Assert;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.RuleChain;
-
-import com.mongodb.DB;
-import com.mongodb.DBCollection;
-import com.mongodb.DBObject;
-import com.mongodb.MongoClient;
-
-/**
- * This class name does NOT end in "Test" in order to only be picked up by {@link Java8Test}.
- */
-@Category(Appenders.MongoDb.class)
-public class MongoDbCappedTest {
-
-    private static LoggerContextRule loggerContextTestRule = new LoggerContextRule("log4j2-mongodb-capped.xml");
-
-    private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
-
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(), LoggingTarget.NULL);
-
-    @ClassRule
-    public static RuleChain ruleChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule,
-            loggerContextTestRule);
-
-    @Test
-    public void test() {
-        final Logger logger = LogManager.getLogger();
-        logger.info("Hello log");
-        final MongoClient mongoClient = mongoDbTestRule.getMongoClient();
-        try {
-            final DB database = mongoClient.getDB("test");
-            Assert.assertNotNull(database);
-            final DBCollection collection = database.getCollection("applog");
-            Assert.assertNotNull(collection);
-            Assert.assertTrue(collection.find().hasNext());
-            final DBObject first = collection.find().next();
-            Assert.assertNotNull(first);
-            Assert.assertEquals(first.toMap().toString(), "Hello log", first.get("message"));
-        } finally {
-            mongoClient.close();
-        }
-    }
-}
diff --git a/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbMapMessageTest.java b/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbMapMessageTest.java
deleted file mode 100644
index 99350aa..0000000
--- a/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbMapMessageTest.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-package org.apache.logging.log4j.mongodb2;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.categories.Appenders;
-import org.apache.logging.log4j.junit.LoggerContextRule;
-import org.apache.logging.log4j.message.MapMessage;
-import org.apache.logging.log4j.mongodb2.MongoDbTestRule.LoggingTarget;
-import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
-import org.apache.logging.log4j.test.RuleChainFactory;
-import org.junit.Assert;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.RuleChain;
-
-import com.mongodb.DB;
-import com.mongodb.DBCollection;
-import com.mongodb.DBObject;
-import com.mongodb.MongoClient;
-
-/**
- * This class name does NOT end in "Test" in order to only be picked up by {@link Java8Test}.
- */
-@Category(Appenders.MongoDb.class)
-public class MongoDbMapMessageTest {
-
-    private static LoggerContextRule loggerContextTestRule = new LoggerContextRule("log4j2-mongodb-map-message.xml");
-
-    private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
-
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(), LoggingTarget.NULL);
-
-    @ClassRule
-    public static RuleChain ruleChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule,
-            loggerContextTestRule);
-
-    @Test
-    public void test() {
-        final Logger logger = LogManager.getLogger();
-        final MapMessage mapMessage = new MapMessage();
-        mapMessage.with("SomeName", "SomeValue");
-        mapMessage.with("SomeInt", 1);
-        logger.info(mapMessage);
-        //
-        final MongoClient mongoClient = mongoDbTestRule.getMongoClient();
-        try {
-            final DB database = mongoClient.getDB("test");
-            Assert.assertNotNull(database);
-            final DBCollection collection = database.getCollection("applog");
-            Assert.assertNotNull(collection);
-            final DBObject first = collection.find().next();
-            Assert.assertNotNull(first);
-            final String firstMapString = first.toMap().toString();
-            Assert.assertEquals(firstMapString, "SomeValue", first.get("SomeName"));
-            Assert.assertEquals(firstMapString, Integer.valueOf(1), first.get("SomeInt"));
-        } finally {
-            mongoClient.close();
-        }
-    }
-}
diff --git a/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbTest.java b/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbTest.java
deleted file mode 100644
index 2a73f8c..0000000
--- a/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-package org.apache.logging.log4j.mongodb2;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.categories.Appenders;
-import org.apache.logging.log4j.junit.LoggerContextRule;
-import org.apache.logging.log4j.mongodb2.MongoDbTestRule.LoggingTarget;
-import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
-import org.apache.logging.log4j.test.RuleChainFactory;
-import org.junit.Assert;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.experimental.categories.Category;
-import org.junit.rules.RuleChain;
-
-import com.mongodb.DB;
-import com.mongodb.DBCollection;
-import com.mongodb.DBObject;
-import com.mongodb.MongoClient;
-
-/**
- * This class name does NOT end in "Test" in order to only be picked up by {@link Java8Test}.
- */
-@Category(Appenders.MongoDb.class)
-public class MongoDbTest {
-
-    private static LoggerContextRule loggerContextTestRule = new LoggerContextRule("log4j2-mongodb.xml");
-
-    private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
-
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(), LoggingTarget.NULL);
-
-    @ClassRule
-    public static RuleChain ruleChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule,
-            loggerContextTestRule);
-
-    @Test
-    public void test() {
-        final Logger logger = LogManager.getLogger();
-        logger.info("Hello log");
-        final MongoClient mongoClient = mongoDbTestRule.getMongoClient();
-        try {
-            final DB database = mongoClient.getDB("test");
-            Assert.assertNotNull(database);
-            final DBCollection collection = database.getCollection("applog");
-            Assert.assertNotNull(collection);
-            final DBObject first = collection.find().next();
-            Assert.assertNotNull(first);
-            Assert.assertEquals(first.toMap().toString(), "Hello log", first.get("message"));
-        } finally {
-            mongoClient.close();
-        }
-    }
-}
diff --git a/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbTestRule.java b/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbTestRule.java
deleted file mode 100644
index acab7ba..0000000
--- a/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbTestRule.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-
-package org.apache.logging.log4j.mongodb2;
-
-import java.util.Objects;
-
-import org.apache.commons.lang3.NotImplementedException;
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.mongodb.MongoClient;
-
-import de.flapdoodle.embed.mongo.Command;
-import de.flapdoodle.embed.mongo.MongodExecutable;
-import de.flapdoodle.embed.mongo.MongodProcess;
-import de.flapdoodle.embed.mongo.MongodStarter;
-import de.flapdoodle.embed.mongo.config.MongodConfigBuilder;
-import de.flapdoodle.embed.mongo.config.Net;
-import de.flapdoodle.embed.mongo.config.RuntimeConfigBuilder;
-import de.flapdoodle.embed.mongo.config.Timeout;
-import de.flapdoodle.embed.mongo.distribution.Version;
-import de.flapdoodle.embed.process.config.IRuntimeConfig;
-import de.flapdoodle.embed.process.config.io.ProcessOutput;
-import de.flapdoodle.embed.process.runtime.Network;
-
-/**
- * A JUnit test rule to manage a MongoDB embedded instance.
- * 
- * TODO Move this class to Apache Commons Testing.
- */
-public class MongoDbTestRule implements TestRule {
-
-    public enum LoggingTarget {
-        NULL, CONSOLE
-    }
-
-    private static final int BUILDER_TIMEOUT_MILLIS = 30000;
-
-    public static int getBuilderTimeoutMillis() {
-        return BUILDER_TIMEOUT_MILLIS;
-    }
-
-    /**
-     * Store {@link MongodStarter} (or RuntimeConfig) in a static final field if you want to use artifact store caching
-     * (or else disable caching).
-     * <p>
-     * The test framework {@code de.flapdoodle.embed.mongo} requires Java 8.
-     * </p>
-     */
-    protected final MongodStarter starter;
-
-    protected final String portSystemPropertyName;
-
-    protected MongoClient mongoClient;
-    protected MongodExecutable mongodExecutable;
-    protected MongodProcess mongodProcess;
-    protected final LoggingTarget loggingTarget;
-
-    /**
-     * Constructs a new test rule.
-     *
-     * @param portSystemPropertyName
-     *            The system property name for the MongoDB port.
-     * @param loggingTarget
-     *            The logging target
-     */
-    public MongoDbTestRule(final String portSystemPropertyName, final LoggingTarget loggingTarget) {
-        this.portSystemPropertyName = Objects.requireNonNull(portSystemPropertyName, "portSystemPropertyName");
-        this.loggingTarget = loggingTarget;
-        this.starter = getMongodStarter(loggingTarget);
-    }
-
-    private static MongodStarter getMongodStarter(final LoggingTarget loggingTarget) {
-        if (loggingTarget == null) {
-            return MongodStarter.getDefaultInstance();
-        }
-        switch (loggingTarget) {
-        case NULL:
-            final Logger logger = LoggerFactory.getLogger(MongoDbTestRule.class.getName());
-            final IRuntimeConfig runtimeConfig = new RuntimeConfigBuilder()
-            // @formatter:off
-                .defaultsWithLogger(Command.MongoD, logger)
-                .processOutput(ProcessOutput.getDefaultInstanceSilent())
-                .build();
-            // @formatter:on
-
-            return MongodStarter.getInstance(runtimeConfig);
-        case CONSOLE:
-            return MongodStarter.getDefaultInstance();
-        default:
-            throw new NotImplementedException(loggingTarget.toString());
-        }
-    }
-
-    @Override
-    public Statement apply(final Statement base, final Description description) {
-        return new Statement() {
-
-            @Override
-            public void evaluate() throws Throwable {
-                final String value = Objects.requireNonNull(System.getProperty(portSystemPropertyName),
-                        "System property '" + portSystemPropertyName + "' is null");
-                final int port = Integer.parseInt(value);
-                mongodExecutable = starter.prepare(
-                // @formatter:off
-                        new MongodConfigBuilder()
-                            .version(Version.Main.PRODUCTION)
-                            .timeout(new Timeout(BUILDER_TIMEOUT_MILLIS))
-                            .net(
-                                    new Net("localhost", port, Network.localhostIsIPv6()))
-                            .build());
-                // @formatter:on
-                mongodProcess = mongodExecutable.start();
-                mongoClient = new MongoClient("localhost", port);
-                try {
-                    base.evaluate();
-                } finally {
-                    if (mongodProcess != null) {
-                        mongodProcess.stop();
-                        mongodProcess = null;
-                    }
-                    if (mongodExecutable != null) {
-                        mongodExecutable.stop();
-                        mongodExecutable = null;
-                    }
-                }
-            }
-        };
-    }
-
-    public MongoClient getMongoClient() {
-        return mongoClient;
-    }
-
-    public MongodExecutable getMongodExecutable() {
-        return mongodExecutable;
-    }
-
-    public MongodProcess getMongodProcess() {
-        return mongodProcess;
-    }
-
-    public MongodStarter getStarter() {
-        return starter;
-    }
-
-    @Override
-    public String toString() {
-        StringBuilder builder = new StringBuilder();
-        builder.append("MongoDbTestRule [starter=");
-        builder.append(starter);
-        builder.append(", portSystemPropertyName=");
-        builder.append(portSystemPropertyName);
-        builder.append(", mongoClient=");
-        builder.append(mongoClient);
-        builder.append(", mongodExecutable=");
-        builder.append(mongodExecutable);
-        builder.append(", mongodProcess=");
-        builder.append(mongodProcess);
-        builder.append(", loggingTarget=");
-        builder.append(loggingTarget);
-        builder.append("]");
-        return builder.toString();
-    }
-
-}
diff --git a/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbTestTestRuleTest.java b/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbTestTestRuleTest.java
deleted file mode 100644
index 68e9782..0000000
--- a/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/MongoDbTestTestRuleTest.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-
-package org.apache.logging.log4j.mongodb2;
-
-import java.util.List;
-
-import org.apache.commons.lang3.JavaVersion;
-import org.apache.commons.lang3.SystemUtils;
-import org.apache.logging.log4j.mongodb2.MongoDbTestRule.LoggingTarget;
-import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
-import org.apache.logging.log4j.test.RuleChainFactory;
-import org.junit.Assert;
-import org.junit.Assume;
-import org.junit.BeforeClass;
-import org.junit.ClassRule;
-import org.junit.Test;
-import org.junit.rules.RuleChain;
-
-/**
- * Tests {@link MongoDbTestRule}. This class name does NOT end in "Test" in order to only be picked up by {@link Java8Test}.
- * <p>
- * The test framework {@code de.flapdoodle.embed.mongo} requires Java 8.
- * </p>
- */
-public class MongoDbTestTestRuleTest {
-
-    private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
-
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(), LoggingTarget.NULL);
-
-    @ClassRule
-    public static RuleChain mongoDbChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule);
-
-    @BeforeClass
-    public static void beforeClass() {
-        Assume.assumeTrue(SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_1_8));
-    }
-
-    @Test
-    public void testAccess() {
-        final List<String> databaseNames = mongoDbTestRule.getMongoClient().getDatabaseNames();
-        Assert.assertNotNull(databaseNames);
-        Assert.assertFalse(databaseNames.isEmpty());
-        Assert.assertNotNull(databaseNames.get(0));
-    }
-
-    @Test
-    public void testMongoDbTestRule() {
-        Assert.assertNotNull(mongoDbTestRule);
-        Assert.assertNotNull(mongoDbTestRule.getStarter());
-        Assert.assertNotNull(mongoDbTestRule.getMongoClient());
-        Assert.assertNotNull(mongoDbTestRule.getMongodExecutable());
-        Assert.assertNotNull(mongoDbTestRule.getMongodProcess());
-    }
-}
diff --git a/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/TestConstants.java b/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/TestConstants.java
deleted file mode 100644
index 040209c..0000000
--- a/log4j-mongodb2/src/test/java/org/apache/logging/log4j/mongodb2/TestConstants.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-
-package org.apache.logging.log4j.mongodb2;
-
-public class TestConstants {
-
-    public static final String SYS_PROP_NAME_PORT = "MongoDBTestPort";
-
-}
diff --git a/log4j-mongodb2/src/test/resources/log4j2-mongodb-auth-failure.xml b/log4j-mongodb2/src/test/resources/log4j2-mongodb-auth-failure.xml
deleted file mode 100644
index 43d7e37..0000000
--- a/log4j-mongodb2/src/test/resources/log4j2-mongodb-auth-failure.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?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.
-
--->
-<Configuration status="WARN">
-  <Appenders>
-    <NoSql name="MongoDbAppender">
-      <MongoDb2 databaseName="test" collectionName="applog" server="localhost" userName="log4jUser" password="12345678"
-        port="${sys:MongoDBTestPort:-27017}" />
-    </NoSql>
-  </Appenders>
-  <Loggers>
-    <Root level="ALL">
-      <AppenderRef ref="MongoDbAppender" />
-    </Root>
-  </Loggers>
-</Configuration>
diff --git a/log4j-mongodb2/src/test/resources/log4j2-mongodb-capped.xml b/log4j-mongodb2/src/test/resources/log4j2-mongodb-capped.xml
deleted file mode 100644
index 7eda0ae..0000000
--- a/log4j-mongodb2/src/test/resources/log4j2-mongodb-capped.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?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.
-
--->
-<Configuration status="WARN">
-  <Appenders>
-    <NoSql name="MongoDbAppender">
-      <MongoDb2 databaseName="test" collectionName="applog" server="localhost" capped="true" collectionSize="1073741824"
-        port="${sys:MongoDBTestPort:-27017}" />
-    </NoSql>
-  </Appenders>
-  <Loggers>
-    <Root level="ALL">
-      <AppenderRef ref="MongoDbAppender" />
-    </Root>
-  </Loggers>
-</Configuration>
diff --git a/log4j-mongodb3/pom.xml b/log4j-mongodb3/pom.xml
index e34cf61..e634c50 100644
--- a/log4j-mongodb3/pom.xml
+++ b/log4j-mongodb3/pom.xml
@@ -44,10 +44,12 @@
     <dependency>
       <groupId>org.mongodb</groupId>
       <artifactId>mongodb-driver</artifactId>
+      <version>${mongodb3.version}</version>
     </dependency>
     <dependency>
       <groupId>org.mongodb</groupId>
       <artifactId>bson</artifactId>
+      <version>${mongodb3.version}</version>
     </dependency>
     <!-- Test Dependencies -->
     <dependency>
@@ -63,15 +65,12 @@
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-api</artifactId>
       <type>test-jar</type>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-core</artifactId>
       <type>test-jar</type>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.logging.log4j</groupId>
-      <artifactId>log4j-slf4j-impl</artifactId>
       <scope>test</scope>
     </dependency>
     <dependency>
diff --git a/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbConnection.java b/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDb3Connection.java
similarity index 81%
rename from log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbConnection.java
rename to log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDb3Connection.java
index e25c366..512d3ff 100644
--- a/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbConnection.java
+++ b/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDb3Connection.java
@@ -16,16 +16,13 @@
  */
 package org.apache.logging.log4j.mongodb3;
 
-import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.appender.AppenderLoggingException;
 import org.apache.logging.log4j.core.appender.nosql.AbstractNoSqlConnection;
 import org.apache.logging.log4j.core.appender.nosql.NoSqlConnection;
 import org.apache.logging.log4j.core.appender.nosql.NoSqlObject;
 import org.apache.logging.log4j.status.StatusLogger;
-import org.bson.BSON;
 import org.bson.Document;
-import org.bson.Transformer;
 
 import com.mongodb.MongoClient;
 import com.mongodb.MongoException;
@@ -36,22 +33,10 @@
 /**
  * The MongoDB implementation of {@link NoSqlConnection}.
  */
-public final class MongoDbConnection extends AbstractNoSqlConnection<Document, MongoDbDocumentObject> {
+public final class MongoDb3Connection extends AbstractNoSqlConnection<Document, MongoDb3DocumentObject> {
 
     private static final Logger LOGGER = StatusLogger.getLogger();
 
-    static {
-        BSON.addEncodingHook(Level.class, new Transformer() {
-            @Override
-            public Object transform(final Object o) {
-                if (o instanceof Level) {
-                    return ((Level) o).name();
-                }
-                return o;
-            }
-        });
-    }
-
     private static MongoCollection<Document> getOrCreateMongoCollection(final MongoDatabase database,
             final String collectionName, final boolean isCapped, final Integer sizeInBytes) {
         try {
@@ -76,7 +61,7 @@
     private final MongoCollection<Document> collection;
     private final MongoClient mongoClient;
 
-    public MongoDbConnection(final MongoClient mongoClient, final MongoDatabase mongoDatabase,
+    public MongoDb3Connection(final MongoClient mongoClient, final MongoDatabase mongoDatabase,
             final String collectionName, final boolean isCapped, final Integer sizeInBytes) {
         this.mongoClient = mongoClient;
         this.collection = getOrCreateMongoCollection(mongoDatabase, collectionName, isCapped, sizeInBytes);
@@ -89,13 +74,13 @@
     }
 
     @Override
-    public MongoDbDocumentObject[] createList(final int length) {
-        return new MongoDbDocumentObject[length];
+    public MongoDb3DocumentObject[] createList(final int length) {
+        return new MongoDb3DocumentObject[length];
     }
 
     @Override
-    public MongoDbDocumentObject createObject() {
-        return new MongoDbDocumentObject();
+    public MongoDb3DocumentObject createObject() {
+        return new MongoDb3DocumentObject();
     }
 
     @Override
diff --git a/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbDocumentObject.java b/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDb3DocumentObject.java
similarity index 93%
rename from log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbDocumentObject.java
rename to log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDb3DocumentObject.java
index 49bdc88..3132d26 100644
--- a/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbDocumentObject.java
+++ b/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDb3DocumentObject.java
@@ -24,10 +24,10 @@
 /**
  * The MongoDB implementation of {@link NoSqlObject} typed to a BSON {@link Document}.
  */
-public final class MongoDbDocumentObject implements NoSqlObject<Document> {
+public final class MongoDb3DocumentObject implements NoSqlObject<Document> {
     private final Document document;
 
-    public MongoDbDocumentObject() {
+    public MongoDb3DocumentObject() {
         this.document = new Document();
     }
 
diff --git a/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/LevelCodec.java b/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDb3LevelCodec.java
similarity index 87%
rename from log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/LevelCodec.java
rename to log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDb3LevelCodec.java
index 9bbc0b0..0ecefcf 100644
--- a/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/LevelCodec.java
+++ b/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDb3LevelCodec.java
@@ -27,8 +27,15 @@
 /**
  * A BSON Codec for Log4j {@link Level}s.
  */
-public class LevelCodec implements Codec<Level> {
+public class MongoDb3LevelCodec implements Codec<Level> {
 
+    /**
+     * The singleton instance.
+     *
+     * @since 2.14.0
+     */
+    public static final MongoDb3LevelCodec INSTANCE = new MongoDb3LevelCodec();
+    
     @Override
     public Level decode(final BsonReader reader, final DecoderContext decoderContext) {
         return Level.getLevel(reader.readString());
diff --git a/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbProvider.java b/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDb3Provider.java
similarity index 91%
rename from log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbProvider.java
rename to log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDb3Provider.java
index af392c0..d38cab8 100644
--- a/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbProvider.java
+++ b/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDb3Provider.java
@@ -16,12 +16,9 @@
  */
 package org.apache.logging.log4j.mongodb3;
 
-import com.mongodb.MongoClient;
-import com.mongodb.MongoClientOptions;
-import com.mongodb.MongoCredential;
-import com.mongodb.ServerAddress;
-import com.mongodb.WriteConcern;
-import com.mongodb.client.MongoDatabase;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.Core;
 import org.apache.logging.log4j.core.appender.nosql.NoSqlProvider;
@@ -38,18 +35,29 @@
 import org.apache.logging.log4j.util.NameUtil;
 import org.apache.logging.log4j.util.Strings;
 import org.bson.codecs.configuration.CodecRegistries;
+import org.bson.codecs.configuration.CodecRegistry;
 
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
+import com.mongodb.MongoClient;
+import com.mongodb.MongoClientOptions;
+import com.mongodb.MongoCredential;
+import com.mongodb.ServerAddress;
+import com.mongodb.WriteConcern;
+import com.mongodb.client.MongoDatabase;
 
 /**
- * The MongoDB implementation of {@link NoSqlProvider}.
+ * The MongoDB implementation of {@link NoSqlProvider} using the MongoDB driver version 3 API.
  */
 @Plugin(name = "MongoDb3", category = Core.CATEGORY_NAME, printObject = true)
-public final class MongoDbProvider implements NoSqlProvider<MongoDbConnection> {
+public final class MongoDb3Provider implements NoSqlProvider<MongoDb3Connection> {
 
     public static class Builder<B extends Builder<B>> extends AbstractFilterable.Builder<B>
-            implements org.apache.logging.log4j.plugins.util.Builder<MongoDbProvider> {
+            implements org.apache.logging.log4j.plugins.util.Builder<MongoDb3Provider> {
+
+        // @formatter:off
+        private static final CodecRegistry CODEC_REGISTRIES = CodecRegistries.fromRegistries(
+                        CodecRegistries.fromCodecs(MongoDb3LevelCodec.INSTANCE),
+                        MongoClient.getDefaultCodecRegistry());
+        // @formatter:on
 
         private static WriteConcern toWriteConcern(final String writeConcernConstant,
                 final String writeConcernConstantClassName) {
@@ -120,7 +128,7 @@
 
         @SuppressWarnings("resource")
         @Override
-        public MongoDbProvider build() {
+        public MongoDb3Provider build() {
             MongoDatabase database;
             String description;
             MongoClient mongoClient = null;
@@ -151,8 +159,8 @@
                         return null;
                     }
 
-                    final String databaseName = database.getName();
-                    description = "database=" + databaseName;
+                    final String dbName = database.getName();
+                    description = "database=" + dbName;
                 } catch (final ClassNotFoundException e) {
                     LOGGER.error("The factory class [{}] could not be loaded.", factoryClassName, e);
                     return null;
@@ -170,7 +178,7 @@
                 description = "database=" + databaseName;
                 if (Strings.isNotEmpty(userName) && Strings.isNotEmpty(password)) {
                     description += ", username=" + userName + ", passwordHash="
-                            + NameUtil.md5(password + MongoDbProvider.class.getName());
+                            + NameUtil.md5(password + MongoDb3Provider.class.getName());
                     mongoCredential = MongoCredential.createCredential(userName, databaseName, password.toCharArray());
                 }
                 try {
@@ -179,9 +187,7 @@
                     final WriteConcern writeConcern = toWriteConcern(writeConcernConstant, writeConcernConstantClassName);
                     // @formatter:off
                     final MongoClientOptions options = MongoClientOptions.builder()
-                            .codecRegistry(CodecRegistries.fromRegistries(
-                                            CodecRegistries.fromCodecs(new LevelCodec()),
-                                            MongoClient.getDefaultCodecRegistry()))
+                            .codecRegistry(CODEC_REGISTRIES)
                             .writeConcern(writeConcern)
                             .build();
                     // @formatter:on
@@ -214,7 +220,7 @@
                 return null;
             }
 
-            return new MongoDbProvider(mongoClient, database, collectionName, capped, collectionSize, description);
+            return new MongoDb3Provider(mongoClient, database, collectionName, capped, collectionSize, description);
         }
 
         private void close(final MongoClient mongoClient) {
@@ -302,7 +308,7 @@
     private final MongoClient mongoClient;
     private final MongoDatabase mongoDatabase;
 
-    private MongoDbProvider(final MongoClient mongoClient, final MongoDatabase mongoDatabase,
+    private MongoDb3Provider(final MongoClient mongoClient, final MongoDatabase mongoDatabase,
             final String collectionName, final boolean isCapped, final Integer collectionSize,
             final String description) {
         this.mongoClient = mongoClient;
@@ -314,8 +320,8 @@
     }
 
     @Override
-    public MongoDbConnection getConnection() {
-        return new MongoDbConnection(mongoClient, mongoDatabase, collectionName, isCapped, collectionSize);
+    public MongoDb3Connection getConnection() {
+        return new MongoDb3Connection(mongoClient, mongoDatabase, collectionName, isCapped, collectionSize);
     }
 
     @Override
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbAuthFailureTest.java b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3AuthFailureTest.java
similarity index 88%
rename from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbAuthFailureTest.java
rename to log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3AuthFailureTest.java
index 19a45c8..540be57 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbAuthFailureTest.java
+++ b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3AuthFailureTest.java
@@ -20,7 +20,7 @@
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.categories.Appenders;
 import org.apache.logging.log4j.junit.LoggerContextRule;
-import org.apache.logging.log4j.mongodb3.MongoDbTestRule.LoggingTarget;
+import org.apache.logging.log4j.mongodb3.MongoDb3TestRule.LoggingTarget;
 import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
 import org.apache.logging.log4j.test.RuleChainFactory;
 import org.bson.Document;
@@ -42,15 +42,15 @@
  */
 @Ignore("TODO Set up the log4j user in MongoDB")
 @Category(Appenders.MongoDb.class)
-public class MongoDbAuthFailureTest {
+public class MongoDb3AuthFailureTest {
 
     private static LoggerContextRule loggerContextTestRule = new LoggerContextRule("log4j2-mongodb-auth-failure.xml");
 
     private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
+            .create(MongoDb3TestConstants.SYS_PROP_NAME_PORT);
 
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(),
-            MongoDbAuthFailureTest.class, LoggingTarget.NULL);
+    private static final MongoDb3TestRule mongoDbTestRule = new MongoDb3TestRule(mongoDbPortTestRule.getName(),
+            MongoDb3AuthFailureTest.class, LoggingTarget.NULL);
 
     @ClassRule
     public static RuleChain ruleChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule,
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbCappedTest.java b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3CappedTest.java
similarity index 88%
rename from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbCappedTest.java
rename to log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3CappedTest.java
index 7611284..f03af5f 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbCappedTest.java
+++ b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3CappedTest.java
@@ -20,7 +20,7 @@
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.categories.Appenders;
 import org.apache.logging.log4j.junit.LoggerContextRule;
-import org.apache.logging.log4j.mongodb3.MongoDbTestRule.LoggingTarget;
+import org.apache.logging.log4j.mongodb3.MongoDb3TestRule.LoggingTarget;
 import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
 import org.apache.logging.log4j.test.RuleChainFactory;
 import org.bson.Document;
@@ -38,15 +38,15 @@
  * This class name does NOT end in "Test" in order to only be picked up by {@link Java8Test}.
  */
 @Category(Appenders.MongoDb.class)
-public class MongoDbCappedTest {
+public class MongoDb3CappedTest {
 
     private static LoggerContextRule loggerContextTestRule = new LoggerContextRule("log4j2-mongodb-capped.xml");
 
     private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
+            .create(MongoDb3TestConstants.SYS_PROP_NAME_PORT);
 
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(),
-            MongoDbCappedTest.class, LoggingTarget.NULL);
+    private static final MongoDb3TestRule mongoDbTestRule = new MongoDb3TestRule(mongoDbPortTestRule.getName(),
+            MongoDb3CappedTest.class, LoggingTarget.NULL);
 
     @ClassRule
     public static RuleChain ruleChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule,
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbMapMessageTest.java b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3MapMessageTest.java
similarity index 87%
rename from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbMapMessageTest.java
rename to log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3MapMessageTest.java
index 70f910e..9a45ede 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbMapMessageTest.java
+++ b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3MapMessageTest.java
@@ -21,7 +21,7 @@
 import org.apache.logging.log4j.categories.Appenders;
 import org.apache.logging.log4j.junit.LoggerContextRule;
 import org.apache.logging.log4j.message.MapMessage;
-import org.apache.logging.log4j.mongodb3.MongoDbTestRule.LoggingTarget;
+import org.apache.logging.log4j.mongodb3.MongoDb3TestRule.LoggingTarget;
 import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
 import org.apache.logging.log4j.test.RuleChainFactory;
 import org.bson.Document;
@@ -39,15 +39,15 @@
  * This class name does NOT end in "Test" in order to only be picked up by {@link Java8Test}.
  */
 @Category(Appenders.MongoDb.class)
-public class MongoDbMapMessageTest {
+public class MongoDb3MapMessageTest {
 
     private static LoggerContextRule loggerContextTestRule = new LoggerContextRule("log4j2-mongodb-map-message.xml");
 
     private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
+            .create(MongoDb3TestConstants.SYS_PROP_NAME_PORT);
 
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(),
-            MongoDbMapMessageTest.class, LoggingTarget.NULL);
+    private static final MongoDb3TestRule mongoDbTestRule = new MongoDb3TestRule(mongoDbPortTestRule.getName(),
+            MongoDb3MapMessageTest.class, LoggingTarget.NULL);
 
     @ClassRule
     public static RuleChain ruleChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule,
@@ -56,7 +56,7 @@
     @Test
     public void test() {
         final Logger logger = LogManager.getLogger();
-        final MapMessage mapMessage = new MapMessage();
+        final MapMessage<?, Object> mapMessage = new MapMessage<>();
         mapMessage.with("SomeName", "SomeValue");
         mapMessage.with("SomeInt", 1);
         logger.info(mapMessage);
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTest.java b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3Test.java
similarity index 86%
rename from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTest.java
rename to log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3Test.java
index eab64b3..dd0bcd2 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTest.java
+++ b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3Test.java
@@ -20,7 +20,7 @@
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.categories.Appenders;
 import org.apache.logging.log4j.junit.LoggerContextRule;
-import org.apache.logging.log4j.mongodb3.MongoDbTestRule.LoggingTarget;
+import org.apache.logging.log4j.mongodb3.MongoDb3TestRule.LoggingTarget;
 import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
 import org.apache.logging.log4j.test.RuleChainFactory;
 import org.bson.Document;
@@ -38,15 +38,15 @@
  * This class name does NOT end in "Test" in order to only be picked up by {@link Java8Test}.
  */
 @Category(Appenders.MongoDb.class)
-public class MongoDbTest {
+public class MongoDb3Test {
 
     private static LoggerContextRule loggerContextTestRule = new LoggerContextRule("log4j2-mongodb.xml");
 
     private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
+            .create(MongoDb3TestConstants.SYS_PROP_NAME_PORT);
 
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(),
-            MongoDbTest.class, LoggingTarget.NULL);
+    private static final MongoDb3TestRule mongoDbTestRule = new MongoDb3TestRule(mongoDbPortTestRule.getName(),
+            MongoDb3Test.class, LoggingTarget.NULL);
 
     @ClassRule
     public static RuleChain ruleChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule,
@@ -64,6 +64,7 @@
             final Document first = collection.find().first();
             Assert.assertNotNull(first);
             Assert.assertEquals(first.toJson(), "Hello log", first.getString("message"));
+            Assert.assertEquals(first.toJson(), "INFO", first.getString("level"));
         }
     }
 }
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3TestConstants.java
similarity index 96%
rename from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
rename to log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3TestConstants.java
index 3e7e0f3..cfae3e4 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
+++ b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3TestConstants.java
@@ -17,7 +17,7 @@
 
 package org.apache.logging.log4j.mongodb3;
 
-public class TestConstants {
+public class MongoDb3TestConstants {
 
     public static final String SYS_PROP_NAME_PORT = "MongoDBTestPort";
 
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTestRule.java b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3TestRule.java
similarity index 97%
rename from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTestRule.java
rename to log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3TestRule.java
index 51bbcc2..51c2012 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTestRule.java
+++ b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3TestRule.java
@@ -46,7 +46,7 @@
  * 
  * TODO Move this class to Apache Commons Testing.
  */
-public class MongoDbTestRule implements TestRule {
+public class MongoDb3TestRule implements TestRule {
 
     public enum LoggingTarget {
         CONSOLE, NULL;
@@ -68,7 +68,7 @@
         }
         switch (loggingTarget) {
         case NULL:
-            final Logger logger = LoggerFactory.getLogger(MongoDbTestRule.class.getName());
+            final Logger logger = LoggerFactory.getLogger(MongoDb3TestRule.class.getName());
             final IRuntimeConfig runtimeConfig = new RuntimeConfigBuilder()
             // @formatter:off
                 .defaultsWithLogger(Command.MongoD, logger)
@@ -110,7 +110,7 @@
      * @param defaultLoggingTarget
      *            The logging target.
      */
-    public MongoDbTestRule(final String portSystemPropertyName, final Class<?> clazz,
+    public MongoDb3TestRule(final String portSystemPropertyName, final Class<?> clazz,
             final LoggingTarget defaultLoggingTarget) {
         this.portSystemPropertyName = Objects.requireNonNull(portSystemPropertyName, "portSystemPropertyName");
         this.loggingTarget = LoggingTarget.getLoggingTarget(clazz.getName() + "." + LoggingTarget.class.getSimpleName(),
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTestTestRuleTest.java b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3TestTestRuleTest.java
similarity index 87%
rename from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTestTestRuleTest.java
rename to log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3TestTestRuleTest.java
index e19980b..ccdf0c8 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTestTestRuleTest.java
+++ b/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDb3TestTestRuleTest.java
@@ -19,7 +19,7 @@
 
 import org.apache.commons.lang3.JavaVersion;
 import org.apache.commons.lang3.SystemUtils;
-import org.apache.logging.log4j.mongodb3.MongoDbTestRule.LoggingTarget;
+import org.apache.logging.log4j.mongodb3.MongoDb3TestRule.LoggingTarget;
 import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
 import org.apache.logging.log4j.test.RuleChainFactory;
 import org.junit.Assert;
@@ -37,13 +37,13 @@
  * The test framework {@code de.flapdoodle.embed.mongo} requires Java 8.
  * </p>
  */
-public class MongoDbTestTestRuleTest {
+public class MongoDb3TestTestRuleTest {
 
     private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
+            .create(MongoDb3TestConstants.SYS_PROP_NAME_PORT);
 
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(),
-            MongoDbTestTestRuleTest.class, LoggingTarget.NULL);
+    private static final MongoDb3TestRule mongoDbTestRule = new MongoDb3TestRule(mongoDbPortTestRule.getName(),
+            MongoDb3TestTestRuleTest.class, LoggingTarget.NULL);
 
     @ClassRule
     public static RuleChain mongoDbChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule);
diff --git a/log4j-mongodb2/pom.xml b/log4j-mongodb4/pom.xml
similarity index 92%
rename from log4j-mongodb2/pom.xml
rename to log4j-mongodb4/pom.xml
index b3b678b..bbd9629 100644
--- a/log4j-mongodb2/pom.xml
+++ b/log4j-mongodb4/pom.xml
@@ -24,16 +24,16 @@
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
-  <artifactId>log4j-mongodb2</artifactId>
-  <name>Apache Log4j MongoDB 2</name>
+  <artifactId>log4j-mongodb4</artifactId>
+  <name>Apache Log4j MongoDB 4</name>
   <description>
-    MongoDB appender for Log4j using the MongoDB 2 driver API.
+    MongoDB appender for Log4j using the MongoDB 4 driver API.
   </description>
   <properties>
     <log4jParentDir>${basedir}/..</log4jParentDir>
-    <docLabel>MongoDB 2 Documentation</docLabel>
-    <projectDir>/log4j-mongodb2</projectDir>
-    <module.name>org.apache.logging.log4j.mongodb2</module.name>
+    <docLabel>MongoDB 4 Documentation</docLabel>
+    <projectDir>/log4j-mongodb4</projectDir>
+    <module.name>org.apache.logging.log4j.mongodb4</module.name>
   </properties>
 
   <dependencies>
@@ -43,7 +43,13 @@
     </dependency>
     <dependency>
       <groupId>org.mongodb</groupId>
-      <artifactId>mongo-java-driver</artifactId>
+      <artifactId>mongodb-driver-sync</artifactId>
+      <version>${mongodb4.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.mongodb</groupId>
+      <artifactId>bson</artifactId>
+      <version>${mongodb4.version}</version>
     </dependency>
     <!-- Test Dependencies -->
     <dependency>
@@ -59,15 +65,12 @@
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-api</artifactId>
       <type>test-jar</type>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-core</artifactId>
       <type>test-jar</type>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.logging.log4j</groupId>
-      <artifactId>log4j-slf4j-impl</artifactId>
       <scope>test</scope>
     </dependency>
     <dependency>
diff --git a/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbConnection.java b/log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/MongoDb4Connection.java
similarity index 61%
copy from log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbConnection.java
copy to log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/MongoDb4Connection.java
index e25c366..509003f 100644
--- a/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbConnection.java
+++ b/log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/MongoDb4Connection.java
@@ -14,72 +14,63 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
-package org.apache.logging.log4j.mongodb3;
+package org.apache.logging.log4j.mongodb4;
 
-import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.core.appender.AppenderLoggingException;
 import org.apache.logging.log4j.core.appender.nosql.AbstractNoSqlConnection;
 import org.apache.logging.log4j.core.appender.nosql.NoSqlConnection;
 import org.apache.logging.log4j.core.appender.nosql.NoSqlObject;
 import org.apache.logging.log4j.status.StatusLogger;
-import org.bson.BSON;
 import org.bson.Document;
-import org.bson.Transformer;
 
-import com.mongodb.MongoClient;
+import com.mongodb.ConnectionString;
 import com.mongodb.MongoException;
+import com.mongodb.client.MongoClient;
 import com.mongodb.client.MongoCollection;
 import com.mongodb.client.MongoDatabase;
 import com.mongodb.client.model.CreateCollectionOptions;
+import com.mongodb.client.result.InsertOneResult;
 
 /**
  * The MongoDB implementation of {@link NoSqlConnection}.
  */
-public final class MongoDbConnection extends AbstractNoSqlConnection<Document, MongoDbDocumentObject> {
+public final class MongoDb4Connection extends AbstractNoSqlConnection<Document, MongoDb4DocumentObject> {
 
     private static final Logger LOGGER = StatusLogger.getLogger();
 
-    static {
-        BSON.addEncodingHook(Level.class, new Transformer() {
-            @Override
-            public Object transform(final Object o) {
-                if (o instanceof Level) {
-                    return ((Level) o).name();
-                }
-                return o;
-            }
-        });
-    }
-
     private static MongoCollection<Document> getOrCreateMongoCollection(final MongoDatabase database,
             final String collectionName, final boolean isCapped, final Integer sizeInBytes) {
         try {
             LOGGER.debug("Gettting collection '{}'...", collectionName);
             // throws IllegalArgumentException if collectionName is invalid
-            return database.getCollection(collectionName);
+            final MongoCollection<Document> found = database.getCollection(collectionName);
+            LOGGER.debug("Got collection {}", found);
+            return found;
         } catch (final IllegalStateException e) {
             LOGGER.debug("Collection '{}' does not exist.", collectionName);
-            final CreateCollectionOptions options = new CreateCollectionOptions()
-            // @formatter:off
-                    .capped(isCapped)
+            final CreateCollectionOptions options = new CreateCollectionOptions().capped(isCapped)
                     .sizeInBytes(sizeInBytes);
-            // @formatter:on
-            LOGGER.debug("Creating collection {} (capped = {}, sizeInBytes = {})", collectionName, isCapped,
-                    sizeInBytes);
+            LOGGER.debug("Creating collection '{}' with options {}...", collectionName, options);
             database.createCollection(collectionName, options);
-            return database.getCollection(collectionName);
+            LOGGER.debug("Created collection.");
+            final MongoCollection<Document> created = database.getCollection(collectionName);
+            LOGGER.debug("Got created collection {}", created);
+            return created;
         }
 
     }
 
+    private final ConnectionString connectionString;
     private final MongoCollection<Document> collection;
     private final MongoClient mongoClient;
 
-    public MongoDbConnection(final MongoClient mongoClient, final MongoDatabase mongoDatabase,
-            final String collectionName, final boolean isCapped, final Integer sizeInBytes) {
+    public MongoDb4Connection(final ConnectionString connectionString, final MongoClient mongoClient,
+            final MongoDatabase mongoDatabase, final boolean isCapped, final Integer sizeInBytes) {
+        this.connectionString = connectionString;
         this.mongoClient = mongoClient;
-        this.collection = getOrCreateMongoCollection(mongoDatabase, collectionName, isCapped, sizeInBytes);
+        this.collection = getOrCreateMongoCollection(mongoDatabase, connectionString.getCollection(), isCapped,
+                sizeInBytes);
     }
 
     @Override
@@ -89,25 +80,32 @@
     }
 
     @Override
-    public MongoDbDocumentObject[] createList(final int length) {
-        return new MongoDbDocumentObject[length];
+    public MongoDb4DocumentObject[] createList(final int length) {
+        return new MongoDb4DocumentObject[length];
     }
 
     @Override
-    public MongoDbDocumentObject createObject() {
-        return new MongoDbDocumentObject();
+    public MongoDb4DocumentObject createObject() {
+        return new MongoDb4DocumentObject();
     }
 
     @Override
     public void insertObject(final NoSqlObject<Document> object) {
         try {
             final Document unwrapped = object.unwrap();
-            LOGGER.debug("Inserting object {}", unwrapped);
-            this.collection.insertOne(unwrapped);
+            LOGGER.debug("Inserting BSON Document {}", unwrapped);
+            InsertOneResult insertOneResult = this.collection.insertOne(unwrapped);
+            LOGGER.debug("Insert MongoDb result {}", insertOneResult);
         } catch (final MongoException e) {
             throw new AppenderLoggingException("Failed to write log event to MongoDB due to error: " + e.getMessage(),
                     e);
         }
     }
 
+    @Override
+    public String toString() {
+        return String.format("Mongo4Connection [connectionString=%s, collection=%s, mongoClient=%s]", connectionString,
+                collection, mongoClient);
+    }
+
 }
diff --git a/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbDocumentObject.java b/log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/MongoDb4DocumentObject.java
similarity index 84%
copy from log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbDocumentObject.java
copy to log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/MongoDb4DocumentObject.java
index 49bdc88..707479d 100644
--- a/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/MongoDbDocumentObject.java
+++ b/log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/MongoDb4DocumentObject.java
@@ -14,7 +14,7 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
-package org.apache.logging.log4j.mongodb3;
+package org.apache.logging.log4j.mongodb4;
 
 import java.util.Arrays;
 
@@ -22,12 +22,13 @@
 import org.bson.Document;
 
 /**
- * The MongoDB implementation of {@link NoSqlObject} typed to a BSON {@link Document}.
+ * The MongoDB implementation of {@link NoSqlObject} typed to a BSON
+ * {@link Document}.
  */
-public final class MongoDbDocumentObject implements NoSqlObject<Document> {
+public final class MongoDb4DocumentObject implements NoSqlObject<Document> {
     private final Document document;
 
-    public MongoDbDocumentObject() {
+    public MongoDb4DocumentObject() {
         this.document = new Document();
     }
 
@@ -52,6 +53,11 @@
     }
 
     @Override
+    public String toString() {
+        return String.format("Mongo4DocumentObject [document=%s]", document);
+    }
+
+    @Override
     public Document unwrap() {
         return this.document;
     }
diff --git a/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/LevelCodec.java b/log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/MongoDb4LevelCodec.java
similarity index 83%
copy from log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/LevelCodec.java
copy to log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/MongoDb4LevelCodec.java
index 9bbc0b0..ea1b5ba 100644
--- a/log4j-mongodb3/src/main/java/org/apache/logging/log4j/mongodb3/LevelCodec.java
+++ b/log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/MongoDb4LevelCodec.java
@@ -15,7 +15,7 @@
  * limitations under the license.
  */
 
-package org.apache.logging.log4j.mongodb3;
+package org.apache.logging.log4j.mongodb4;
 
 import org.apache.logging.log4j.Level;
 import org.bson.BsonReader;
@@ -25,9 +25,14 @@
 import org.bson.codecs.EncoderContext;
 
 /**
- * A BSON Codec for Log4j {@link Level}s.
+ * A BSON {@link Codec} for Log4j {@link Level}s.
  */
-public class LevelCodec implements Codec<Level> {
+public class MongoDb4LevelCodec implements Codec<Level> {
+
+    /**
+     * The singleton instance.
+     */
+    public static final MongoDb4LevelCodec INSTANCE = new MongoDb4LevelCodec();
 
     @Override
     public Level decode(final BsonReader reader, final DecoderContext decoderContext) {
diff --git a/log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/MongoDb4Provider.java b/log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/MongoDb4Provider.java
new file mode 100644
index 0000000..3f7fb2f
--- /dev/null
+++ b/log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/MongoDb4Provider.java
@@ -0,0 +1,132 @@
+/*
+ * 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.mongodb4;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.Core;
+import org.apache.logging.log4j.core.appender.nosql.NoSqlProvider;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
+import org.apache.logging.log4j.core.filter.AbstractFilterable;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.validation.constraints.Required;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.bson.codecs.configuration.CodecRegistries;
+import org.bson.codecs.configuration.CodecRegistry;
+
+import com.mongodb.ConnectionString;
+import com.mongodb.MongoClientSettings;
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
+import com.mongodb.client.MongoDatabase;
+
+/**
+ * The MongoDB implementation of {@link NoSqlProvider} using the MongoDB driver
+ * version 4 API.
+ */
+@Plugin(name = "MongoDb4", category = Core.CATEGORY_NAME, printObject = true)
+public final class MongoDb4Provider implements NoSqlProvider<MongoDb4Connection> {
+
+    public static class Builder<B extends Builder<B>> extends AbstractFilterable.Builder<B>
+            implements org.apache.logging.log4j.core.util.Builder<MongoDb4Provider> {
+
+        @PluginBuilderAttribute(value = "connection")
+        @Required(message = "No connection string provided")
+        private String connection;
+
+        @PluginBuilderAttribute
+        private int collectionSize = DEFAULT_COLLECTION_SIZE;
+
+        @PluginBuilderAttribute("capped")
+        private boolean capped = false;
+
+        @Override
+        public MongoDb4Provider build() {
+            return new MongoDb4Provider(connection, capped, collectionSize);
+        }
+
+        public B setCapped(final boolean isCapped) {
+            this.capped = isCapped;
+            return asBuilder();
+        }
+
+        public B setCollectionSize(final int collectionSize) {
+            this.collectionSize = collectionSize;
+            return asBuilder();
+        }
+    }
+
+    private static final Logger LOGGER = StatusLogger.getLogger();
+
+    // @formatter:off
+    private static final CodecRegistry CODEC_REGISTRIES = CodecRegistries.fromRegistries(
+            MongoClientSettings.getDefaultCodecRegistry(),
+            CodecRegistries.fromCodecs(MongoDb4LevelCodec.INSTANCE));
+    // @formatter:on
+
+    // TODO Where does this number come from?
+    private static final int DEFAULT_COLLECTION_SIZE = 536_870_912;
+
+    @PluginBuilderFactory
+    public static <B extends Builder<B>> B newBuilder() {
+        return new Builder<B>().asBuilder();
+    }
+
+    private final Integer collectionSize;
+    private final boolean isCapped;
+    private final MongoClient mongoClient;
+    private final MongoDatabase mongoDatabase;
+    private final ConnectionString connectionString;
+
+    private MongoDb4Provider(final String connectionStringSource, final boolean isCapped,
+            final Integer collectionSize) {
+        LOGGER.debug("Creating ConnectionString {}...", connectionStringSource);
+        this.connectionString = new ConnectionString(connectionStringSource);
+        LOGGER.debug("Created ConnectionString {}", connectionString);
+        LOGGER.debug("Creating MongoClientSettings...");
+        // @formatter:off
+        final MongoClientSettings settings = MongoClientSettings.builder()
+                .applyConnectionString(this.connectionString)
+                .codecRegistry(CODEC_REGISTRIES)
+                .build();
+        // @formatter:on
+        LOGGER.debug("Created MongoClientSettings {}", settings);
+        LOGGER.debug("Creating MongoClient {}...", settings);
+        this.mongoClient = MongoClients.create(settings);
+        LOGGER.debug("Created MongoClient {}", mongoClient);
+        String databaseName = this.connectionString.getDatabase();
+        LOGGER.debug("Getting MongoDatabase {}...", databaseName);
+        this.mongoDatabase = this.mongoClient.getDatabase(databaseName);
+        LOGGER.debug("Got MongoDatabase {}", mongoDatabase);
+        this.isCapped = isCapped;
+        this.collectionSize = collectionSize;
+    }
+
+    @Override
+    public MongoDb4Connection getConnection() {
+        return new MongoDb4Connection(connectionString, mongoClient, mongoDatabase, isCapped, collectionSize);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "%s [connectionString=%s, collectionSize=%s, isCapped=%s, mongoClient=%s, mongoDatabase=%s]",
+                MongoDb4Provider.class.getSimpleName(), connectionString, collectionSize, isCapped, mongoClient,
+                mongoDatabase);
+    }
+
+}
diff --git a/log4j-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/package-info.java b/log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/package-info.java
similarity index 92%
rename from log4j-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/package-info.java
rename to log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/package-info.java
index bf111af..8f380b3 100644
--- a/log4j-mongodb2/src/main/java/org/apache/logging/log4j/mongodb2/package-info.java
+++ b/log4j-mongodb4/src/main/java/org/apache/logging/log4j/mongodb4/package-info.java
@@ -15,6 +15,7 @@
  * limitations under the license.
  */
 /**
- * The classes in this package contain the MongoDB provider for the NoSQL Appender.
+ * The classes in this package contain the MongoDB provider for the NoSQL
+ * Appender.
  */
-package org.apache.logging.log4j.mongodb2;
+package org.apache.logging.log4j.mongodb4;
diff --git a/log4j-mongodb2/src/site/markdown/index.md.vm b/log4j-mongodb4/src/site/markdown/index.md.vm
similarity index 97%
rename from log4j-mongodb2/src/site/markdown/index.md.vm
rename to log4j-mongodb4/src/site/markdown/index.md.vm
index 0d1eb7a..1bdd5a5 100644
--- a/log4j-mongodb2/src/site/markdown/index.md.vm
+++ b/log4j-mongodb4/src/site/markdown/index.md.vm
@@ -30,7 +30,7 @@
       <dependency>
         <groupId>org.mongodb</groupId>
         <artifactId>mongo-java-driver</artifactId>
-        <version>2.14.3</version>
+        <version>2.12.3</version>
       </dependency>
     </dependencies>
   </dependencyManagement>
diff --git a/log4j-mongodb2/src/site/site.xml b/log4j-mongodb4/src/site/site.xml
similarity index 97%
rename from log4j-mongodb2/src/site/site.xml
rename to log4j-mongodb4/src/site/site.xml
index f5db26e..54ea9be 100644
--- a/log4j-mongodb2/src/site/site.xml
+++ b/log4j-mongodb4/src/site/site.xml
@@ -15,7 +15,7 @@
  limitations under the License.
 
 -->
-<project name="Log4j MongoDB 2.x Appender"
+<project name="Log4j MongoDB Appender"
          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">
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbAuthFailureTest.java b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4AuthFailureTest.java
similarity index 81%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbAuthFailureTest.java
copy to log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4AuthFailureTest.java
index 19a45c8..c84d108 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbAuthFailureTest.java
+++ b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4AuthFailureTest.java
@@ -14,13 +14,13 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
-package org.apache.logging.log4j.mongodb3;
+package org.apache.logging.log4j.mongodb4;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.categories.Appenders;
 import org.apache.logging.log4j.junit.LoggerContextRule;
-import org.apache.logging.log4j.mongodb3.MongoDbTestRule.LoggingTarget;
+import org.apache.logging.log4j.mongodb4.MongoDb4TestRule.LoggingTarget;
 import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
 import org.apache.logging.log4j.test.RuleChainFactory;
 import org.bson.Document;
@@ -31,26 +31,26 @@
 import org.junit.experimental.categories.Category;
 import org.junit.rules.RuleChain;
 
-import com.mongodb.MongoClient;
+import com.mongodb.client.MongoClient;
 import com.mongodb.client.MongoCollection;
 import com.mongodb.client.MongoDatabase;
 
 /**
- * This class name does NOT end in "Test" in order to only be picked up by {@link Java8Test}.
+ *
  *
  * TODO Set up the log4j user in MongoDB.
  */
 @Ignore("TODO Set up the log4j user in MongoDB")
 @Category(Appenders.MongoDb.class)
-public class MongoDbAuthFailureTest {
+public class MongoDb4AuthFailureTest {
 
     private static LoggerContextRule loggerContextTestRule = new LoggerContextRule("log4j2-mongodb-auth-failure.xml");
 
     private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
+            .create(MongoDb4TestConstants.SYS_PROP_NAME_PORT);
 
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(),
-            MongoDbAuthFailureTest.class, LoggingTarget.NULL);
+    private static final MongoDb4TestRule mongoDbTestRule = new MongoDb4TestRule(mongoDbPortTestRule.getName(),
+            MongoDb4AuthFailureTest.class, LoggingTarget.NULL);
 
     @ClassRule
     public static RuleChain ruleChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule,
@@ -61,9 +61,9 @@
         final Logger logger = LogManager.getLogger();
         logger.info("Hello log");
         try (final MongoClient mongoClient = mongoDbTestRule.getMongoClient()) {
-            final MongoDatabase database = mongoClient.getDatabase("test");
+            final MongoDatabase database = mongoClient.getDatabase("testDb");
             Assert.assertNotNull(database);
-            final MongoCollection<Document> collection = database.getCollection("applog");
+            final MongoCollection<Document> collection = database.getCollection("testCollection");
             Assert.assertNotNull(collection);
             final Document first = collection.find().first();
             Assert.assertNull(first);
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbCappedTest.java b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4CappedTest.java
similarity index 81%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbCappedTest.java
copy to log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4CappedTest.java
index 7611284..fcd7cd7 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbCappedTest.java
+++ b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4CappedTest.java
@@ -14,13 +14,13 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
-package org.apache.logging.log4j.mongodb3;
+package org.apache.logging.log4j.mongodb4;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.categories.Appenders;
 import org.apache.logging.log4j.junit.LoggerContextRule;
-import org.apache.logging.log4j.mongodb3.MongoDbTestRule.LoggingTarget;
+import org.apache.logging.log4j.mongodb4.MongoDb4TestRule.LoggingTarget;
 import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
 import org.apache.logging.log4j.test.RuleChainFactory;
 import org.bson.Document;
@@ -30,23 +30,23 @@
 import org.junit.experimental.categories.Category;
 import org.junit.rules.RuleChain;
 
-import com.mongodb.MongoClient;
+import com.mongodb.client.MongoClient;
 import com.mongodb.client.MongoCollection;
 import com.mongodb.client.MongoDatabase;
 
 /**
- * This class name does NOT end in "Test" in order to only be picked up by {@link Java8Test}.
+ *
  */
 @Category(Appenders.MongoDb.class)
-public class MongoDbCappedTest {
+public class MongoDb4CappedTest {
 
     private static LoggerContextRule loggerContextTestRule = new LoggerContextRule("log4j2-mongodb-capped.xml");
 
     private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
+            .create(MongoDb4TestConstants.SYS_PROP_NAME_PORT);
 
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(),
-            MongoDbCappedTest.class, LoggingTarget.NULL);
+    private static final MongoDb4TestRule mongoDbTestRule = new MongoDb4TestRule(mongoDbPortTestRule.getName(),
+            MongoDb4CappedTest.class, LoggingTarget.NULL);
 
     @ClassRule
     public static RuleChain ruleChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule,
@@ -57,9 +57,9 @@
         final Logger logger = LogManager.getLogger();
         logger.info("Hello log");
         try (final MongoClient mongoClient = mongoDbTestRule.getMongoClient()) {
-            final MongoDatabase database = mongoClient.getDatabase("test");
+            final MongoDatabase database = mongoClient.getDatabase("testDb");
             Assert.assertNotNull(database);
-            final MongoCollection<Document> collection = database.getCollection("applog");
+            final MongoCollection<Document> collection = database.getCollection("testCollection");
             Assert.assertNotNull(collection);
             final Document first = collection.find().first();
             Assert.assertNotNull(first);
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbMapMessageTest.java b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4MapMessageTest.java
similarity index 81%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbMapMessageTest.java
copy to log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4MapMessageTest.java
index 70f910e..70ab9a7 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbMapMessageTest.java
+++ b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4MapMessageTest.java
@@ -14,14 +14,14 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
-package org.apache.logging.log4j.mongodb3;
+package org.apache.logging.log4j.mongodb4;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.categories.Appenders;
 import org.apache.logging.log4j.junit.LoggerContextRule;
 import org.apache.logging.log4j.message.MapMessage;
-import org.apache.logging.log4j.mongodb3.MongoDbTestRule.LoggingTarget;
+import org.apache.logging.log4j.mongodb4.MongoDb4TestRule.LoggingTarget;
 import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
 import org.apache.logging.log4j.test.RuleChainFactory;
 import org.bson.Document;
@@ -31,23 +31,23 @@
 import org.junit.experimental.categories.Category;
 import org.junit.rules.RuleChain;
 
-import com.mongodb.MongoClient;
+import com.mongodb.client.MongoClient;
 import com.mongodb.client.MongoCollection;
 import com.mongodb.client.MongoDatabase;
 
 /**
- * This class name does NOT end in "Test" in order to only be picked up by {@link Java8Test}.
+ *
  */
 @Category(Appenders.MongoDb.class)
-public class MongoDbMapMessageTest {
+public class MongoDb4MapMessageTest {
 
     private static LoggerContextRule loggerContextTestRule = new LoggerContextRule("log4j2-mongodb-map-message.xml");
 
     private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
+            .create(MongoDb4TestConstants.SYS_PROP_NAME_PORT);
 
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(),
-            MongoDbMapMessageTest.class, LoggingTarget.NULL);
+    private static final MongoDb4TestRule mongoDbTestRule = new MongoDb4TestRule(mongoDbPortTestRule.getName(),
+            MongoDb4MapMessageTest.class, LoggingTarget.NULL);
 
     @ClassRule
     public static RuleChain ruleChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule,
@@ -56,15 +56,15 @@
     @Test
     public void test() {
         final Logger logger = LogManager.getLogger();
-        final MapMessage mapMessage = new MapMessage();
+        final MapMessage<?, Object> mapMessage = new MapMessage<>();
         mapMessage.with("SomeName", "SomeValue");
         mapMessage.with("SomeInt", 1);
         logger.info(mapMessage);
         //
         try (final MongoClient mongoClient = mongoDbTestRule.getMongoClient()) {
-            final MongoDatabase database = mongoClient.getDatabase("test");
+            final MongoDatabase database = mongoClient.getDatabase("testDb");
             Assert.assertNotNull(database);
-            final MongoCollection<Document> collection = database.getCollection("applog");
+            final MongoCollection<Document> collection = database.getCollection("testCollection");
             Assert.assertNotNull(collection);
             final Document first = collection.find().first();
             Assert.assertNotNull(first);
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTest.java b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4Test.java
similarity index 81%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTest.java
copy to log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4Test.java
index eab64b3..e814f1f 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTest.java
+++ b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4Test.java
@@ -14,13 +14,13 @@
  * See the license for the specific language governing permissions and
  * limitations under the license.
  */
-package org.apache.logging.log4j.mongodb3;
+package org.apache.logging.log4j.mongodb4;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.categories.Appenders;
 import org.apache.logging.log4j.junit.LoggerContextRule;
-import org.apache.logging.log4j.mongodb3.MongoDbTestRule.LoggingTarget;
+import org.apache.logging.log4j.mongodb4.MongoDb4TestRule.LoggingTarget;
 import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
 import org.apache.logging.log4j.test.RuleChainFactory;
 import org.bson.Document;
@@ -30,23 +30,23 @@
 import org.junit.experimental.categories.Category;
 import org.junit.rules.RuleChain;
 
-import com.mongodb.MongoClient;
+import com.mongodb.client.MongoClient;
 import com.mongodb.client.MongoCollection;
 import com.mongodb.client.MongoDatabase;
 
 /**
- * This class name does NOT end in "Test" in order to only be picked up by {@link Java8Test}.
+ *
  */
 @Category(Appenders.MongoDb.class)
-public class MongoDbTest {
+public class MongoDb4Test {
 
     private static LoggerContextRule loggerContextTestRule = new LoggerContextRule("log4j2-mongodb.xml");
 
     private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
+            .create(MongoDb4TestConstants.SYS_PROP_NAME_PORT);
 
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(),
-            MongoDbTest.class, LoggingTarget.NULL);
+    private static final MongoDb4TestRule mongoDbTestRule = new MongoDb4TestRule(mongoDbPortTestRule.getName(),
+            MongoDb4Test.class, LoggingTarget.NULL);
 
     @ClassRule
     public static RuleChain ruleChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule,
@@ -57,13 +57,14 @@
         final Logger logger = LogManager.getLogger();
         logger.info("Hello log");
         try (final MongoClient mongoClient = mongoDbTestRule.getMongoClient()) {
-            final MongoDatabase database = mongoClient.getDatabase("test");
+            final MongoDatabase database = mongoClient.getDatabase("testDb");
             Assert.assertNotNull(database);
-            final MongoCollection<Document> collection = database.getCollection("applog");
+            final MongoCollection<Document> collection = database.getCollection("testCollection");
             Assert.assertNotNull(collection);
             final Document first = collection.find().first();
             Assert.assertNotNull(first);
             Assert.assertEquals(first.toJson(), "Hello log", first.getString("message"));
+            Assert.assertEquals(first.toJson(), "INFO", first.getString("level"));
         }
     }
 }
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4TestConstants.java
similarity index 91%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
copy to log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4TestConstants.java
index 3e7e0f3..0d398f0 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/TestConstants.java
+++ b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4TestConstants.java
@@ -15,9 +15,9 @@
  * limitations under the license.
  */
 
-package org.apache.logging.log4j.mongodb3;
+package org.apache.logging.log4j.mongodb4;
 
-public class TestConstants {
+public class MongoDb4TestConstants {
 
     public static final String SYS_PROP_NAME_PORT = "MongoDBTestPort";
 
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTestRule.java b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4TestRule.java
similarity index 81%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTestRule.java
copy to log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4TestRule.java
index 51bbcc2..035ef53 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTestRule.java
+++ b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4TestRule.java
@@ -15,7 +15,7 @@
  * limitations under the license.
  */
 
-package org.apache.logging.log4j.mongodb3;
+package org.apache.logging.log4j.mongodb4;
 
 import java.util.Objects;
 
@@ -26,7 +26,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.mongodb.MongoClient;
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
 
 import de.flapdoodle.embed.mongo.Command;
 import de.flapdoodle.embed.mongo.MongodExecutable;
@@ -43,10 +44,10 @@
 
 /**
  * A JUnit test rule to manage a MongoDB embedded instance.
- * 
+ *
  * TODO Move this class to Apache Commons Testing.
  */
-public class MongoDbTestRule implements TestRule {
+public class MongoDb4TestRule implements TestRule {
 
     public enum LoggingTarget {
         CONSOLE, NULL;
@@ -68,12 +69,11 @@
         }
         switch (loggingTarget) {
         case NULL:
-            final Logger logger = LoggerFactory.getLogger(MongoDbTestRule.class.getName());
+            final Logger logger = LoggerFactory.getLogger(MongoDb4TestRule.class.getName());
             final IRuntimeConfig runtimeConfig = new RuntimeConfigBuilder()
             // @formatter:off
-                .defaultsWithLogger(Command.MongoD, logger)
-                .processOutput(ProcessOutput.getDefaultInstanceSilent())
-                .build();
+                    .defaultsWithLogger(Command.MongoD, logger).processOutput(ProcessOutput.getDefaultInstanceSilent())
+                    .build();
             // @formatter:on
 
             return MongodStarter.getInstance(runtimeConfig);
@@ -92,8 +92,8 @@
     protected final String portSystemPropertyName;
 
     /**
-     * Store {@link MongodStarter} (or RuntimeConfig) in a static final field if you want to use artifact store caching
-     * (or else disable caching).
+     * Store {@link MongodStarter} (or RuntimeConfig) in a static final field if you
+     * want to use artifact store caching (or else disable caching).
      * <p>
      * The test framework {@code de.flapdoodle.embed.mongo} requires Java 8.
      * </p>
@@ -103,14 +103,11 @@
     /**
      * Constructs a new test rule.
      *
-     * @param portSystemPropertyName
-     *            The system property name for the MongoDB port.
-     * @param clazz
-     *            The test case class.
-     * @param defaultLoggingTarget
-     *            The logging target.
+     * @param portSystemPropertyName The system property name for the MongoDB port.
+     * @param clazz                  The test case class.
+     * @param defaultLoggingTarget   The logging target.
      */
-    public MongoDbTestRule(final String portSystemPropertyName, final Class<?> clazz,
+    public MongoDb4TestRule(final String portSystemPropertyName, final Class<?> clazz,
             final LoggingTarget defaultLoggingTarget) {
         this.portSystemPropertyName = Objects.requireNonNull(portSystemPropertyName, "portSystemPropertyName");
         this.loggingTarget = LoggingTarget.getLoggingTarget(clazz.getName() + "." + LoggingTarget.class.getSimpleName(),
@@ -129,15 +126,12 @@
                 final int port = Integer.parseInt(value);
                 mongodExecutable = starter.prepare(
                 // @formatter:off
-                        new MongodConfigBuilder()
-                            .version(Version.Main.PRODUCTION)
-                            .timeout(new Timeout(BUILDER_TIMEOUT_MILLIS))
-                            .net(
-                                    new Net("localhost", port, Network.localhostIsIPv6()))
-                            .build());
+                        new MongodConfigBuilder().version(Version.Main.PRODUCTION)
+                                .timeout(new Timeout(BUILDER_TIMEOUT_MILLIS))
+                                .net(new Net("localhost", port, Network.localhostIsIPv6())).build());
                 // @formatter:on
                 mongodProcess = mongodExecutable.start();
-                mongoClient = new MongoClient("localhost", port);
+                mongoClient = MongoClients.create("mongodb://localhost:" + port);
                 try {
                     base.evaluate();
                 } finally {
@@ -172,8 +166,8 @@
 
     @Override
     public String toString() {
-        StringBuilder builder = new StringBuilder();
-        builder.append("MongoDbTestRule [starter=");
+        final StringBuilder builder = new StringBuilder();
+        builder.append("Mongo4TestRule [starter=");
         builder.append(starter);
         builder.append(", portSystemPropertyName=");
         builder.append(portSystemPropertyName);
@@ -189,4 +183,4 @@
         return builder.toString();
     }
 
-}
+}
\ No newline at end of file
diff --git a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTestTestRuleTest.java b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4TestTestRuleTest.java
similarity index 82%
copy from log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTestTestRuleTest.java
copy to log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4TestTestRuleTest.java
index e19980b..086af07 100644
--- a/log4j-mongodb3/src/test/java/org/apache/logging/log4j/mongodb3/MongoDbTestTestRuleTest.java
+++ b/log4j-mongodb4/src/test/java/org/apache/logging/log4j/mongodb4/MongoDb4TestTestRuleTest.java
@@ -15,11 +15,11 @@
  * limitations under the license.
  */
 
-package org.apache.logging.log4j.mongodb3;
+package org.apache.logging.log4j.mongodb4;
 
 import org.apache.commons.lang3.JavaVersion;
 import org.apache.commons.lang3.SystemUtils;
-import org.apache.logging.log4j.mongodb3.MongoDbTestRule.LoggingTarget;
+import org.apache.logging.log4j.mongodb4.MongoDb4TestRule.LoggingTarget;
 import org.apache.logging.log4j.test.AvailablePortSystemPropertyTestRule;
 import org.apache.logging.log4j.test.RuleChainFactory;
 import org.junit.Assert;
@@ -32,18 +32,18 @@
 import com.mongodb.client.MongoIterable;
 
 /**
- * Tests MongoDbRule. This class name does NOT end in "Test" in order to only be picked up by {@link Java8Test}.
+ * Tests MongoDbRule.
  * <p>
  * The test framework {@code de.flapdoodle.embed.mongo} requires Java 8.
  * </p>
  */
-public class MongoDbTestTestRuleTest {
+public class MongoDb4TestTestRuleTest {
 
     private static final AvailablePortSystemPropertyTestRule mongoDbPortTestRule = AvailablePortSystemPropertyTestRule
-            .create(TestConstants.SYS_PROP_NAME_PORT);
+            .create(MongoDb4TestConstants.SYS_PROP_NAME_PORT);
 
-    private static final MongoDbTestRule mongoDbTestRule = new MongoDbTestRule(mongoDbPortTestRule.getName(),
-            MongoDbTestTestRuleTest.class, LoggingTarget.NULL);
+    private static final MongoDb4TestRule mongoDbTestRule = new MongoDb4TestRule(mongoDbPortTestRule.getName(),
+            MongoDb4TestTestRuleTest.class, LoggingTarget.NULL);
 
     @ClassRule
     public static RuleChain mongoDbChain = RuleChainFactory.create(mongoDbPortTestRule, mongoDbTestRule);
@@ -55,11 +55,13 @@
 
     @Test
     public void testAccess() {
+        @SuppressWarnings("resource")
         final MongoIterable<String> databaseNames = mongoDbTestRule.getMongoClient().listDatabaseNames();
         Assert.assertNotNull(databaseNames);
         Assert.assertNotNull(databaseNames.first());
     }
 
+    @SuppressWarnings("resource")
     @Test
     public void testMongoDbTestRule() {
         Assert.assertNotNull(mongoDbTestRule);
diff --git a/log4j-mongodb2/src/test/resources/log4j2-mongodb.xml b/log4j-mongodb4/src/test/resources/log4j2-mongodb-auth-failure.xml
similarity index 88%
copy from log4j-mongodb2/src/test/resources/log4j2-mongodb.xml
copy to log4j-mongodb4/src/test/resources/log4j2-mongodb-auth-failure.xml
index 805c746..34be399 100644
--- a/log4j-mongodb2/src/test/resources/log4j2-mongodb.xml
+++ b/log4j-mongodb4/src/test/resources/log4j2-mongodb-auth-failure.xml
@@ -19,8 +19,8 @@
 <Configuration status="WARN">
   <Appenders>
     <NoSql name="MongoDbAppender">
-      <MongoDb2 databaseName="test" collectionName="applog" server="localhost"
-        port="${sys:MongoDBTestPort:-27017}" />
+      <MongoDb4 
+        connection="mongodb://log4jUser:12345678@localhost:${sys:MongoDBTestPort:-27017}/testDb.testCollection" />
     </NoSql>
   </Appenders>
   <Loggers>
diff --git a/log4j-mongodb2/src/test/resources/log4j2-mongodb-map-message.xml b/log4j-mongodb4/src/test/resources/log4j2-mongodb-capped.xml
similarity index 85%
copy from log4j-mongodb2/src/test/resources/log4j2-mongodb-map-message.xml
copy to log4j-mongodb4/src/test/resources/log4j2-mongodb-capped.xml
index 12f957a..d5f5651 100644
--- a/log4j-mongodb2/src/test/resources/log4j2-mongodb-map-message.xml
+++ b/log4j-mongodb4/src/test/resources/log4j2-mongodb-capped.xml
@@ -19,9 +19,10 @@
 <Configuration status="WARN">
   <Appenders>
     <NoSql name="MongoDbAppender">
-      <MongoDb2 databaseName="test" collectionName="applog" server="localhost"
-        port="${sys:MongoDBTestPort:-27017}" />
-      <MessageLayout />
+      <MongoDb4 
+        connection="mongodb://localhost:${sys:MongoDBTestPort:-27017}/testDb.testCollection" 
+        capped="true" 
+        collectionSize="1073741824"/>
     </NoSql>
   </Appenders>
   <Loggers>
diff --git a/log4j-mongodb2/src/test/resources/log4j2-mongodb-map-message.xml b/log4j-mongodb4/src/test/resources/log4j2-mongodb-map-message.xml
similarity index 89%
rename from log4j-mongodb2/src/test/resources/log4j2-mongodb-map-message.xml
rename to log4j-mongodb4/src/test/resources/log4j2-mongodb-map-message.xml
index 12f957a..7534477 100644
--- a/log4j-mongodb2/src/test/resources/log4j2-mongodb-map-message.xml
+++ b/log4j-mongodb4/src/test/resources/log4j2-mongodb-map-message.xml
@@ -19,8 +19,7 @@
 <Configuration status="WARN">
   <Appenders>
     <NoSql name="MongoDbAppender">
-      <MongoDb2 databaseName="test" collectionName="applog" server="localhost"
-        port="${sys:MongoDBTestPort:-27017}" />
+      <MongoDb4 connection="mongodb://localhost:${sys:MongoDBTestPort:-27017}/testDb.testCollection" />
       <MessageLayout />
     </NoSql>
   </Appenders>
diff --git a/log4j-mongodb2/src/test/resources/log4j2-mongodb.xml b/log4j-mongodb4/src/test/resources/log4j2-mongodb.xml
similarity index 89%
rename from log4j-mongodb2/src/test/resources/log4j2-mongodb.xml
rename to log4j-mongodb4/src/test/resources/log4j2-mongodb.xml
index 805c746..514bc8c 100644
--- a/log4j-mongodb2/src/test/resources/log4j2-mongodb.xml
+++ b/log4j-mongodb4/src/test/resources/log4j2-mongodb.xml
@@ -19,8 +19,7 @@
 <Configuration status="WARN">
   <Appenders>
     <NoSql name="MongoDbAppender">
-      <MongoDb2 databaseName="test" collectionName="applog" server="localhost"
-        port="${sys:MongoDBTestPort:-27017}" />
+      <MongoDb4 connection="mongodb://localhost:${sys:MongoDBTestPort:-27017}/testDb.testCollection" />
     </NoSql>
   </Appenders>
   <Loggers>
diff --git a/log4j-perf/pom.xml b/log4j-perf/pom.xml
index 2b599b5..b21f058 100644
--- a/log4j-perf/pom.xml
+++ b/log4j-perf/pom.xml
@@ -79,6 +79,22 @@
       <version>${project.version}</version>
     </dependency>
     <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-layout-jackson-json</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-layout-json-template</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-layout-json-template</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+    </dependency>
+    <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
     </dependency>
@@ -152,6 +168,10 @@
       <artifactId>jackson-databind</artifactId>
       <optional>true</optional>
     </dependency>
+    <dependency>
+      <groupId>co.elastic.logging</groupId>
+      <artifactId>log4j2-ecs-layout</artifactId>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmark.java b/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmark.java
new file mode 100644
index 0000000..5af536b
--- /dev/null
+++ b/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmark.java
@@ -0,0 +1,185 @@
+/*
+ * 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.layout.ByteBufferDestination;
+import org.openjdk.jmh.annotations.Benchmark;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+/**
+ * Benchmark suite for various JSON layouts.
+ * <p>
+ * You can run this test as follows:
+ * <pre>
+ * java \
+ *     -jar log4j-perf/target/benchmarks.jar \
+ *     -f 2 \
+ *     -wi 3 -w 20s \
+ *     -i 5 -r 30s \
+ *     -prof gc \
+ *     -rf json -rff log4j-perf/target/JsonTemplateLayoutBenchmarkResult.json \
+ *     ".*JsonTemplateLayoutBenchmark.*"
+ * </pre>
+ */
+public class JsonTemplateLayoutBenchmark {
+
+    @Benchmark
+    public static int fullJsonTemplateLayout4JsonLayout(
+            final JsonTemplateLayoutBenchmarkState state) {
+        return benchmark(
+                state.getJsonTemplateLayout4JsonLayout(),
+                state.getFullLogEvents(),
+                state.getByteBufferDestination());
+    }
+
+    @Benchmark
+    public static int liteJsonTemplateLayout4JsonLayout(
+            final JsonTemplateLayoutBenchmarkState state) {
+        return benchmark(
+                state.getJsonTemplateLayout4JsonLayout(),
+                state.getLiteLogEvents(),
+                state.getByteBufferDestination());
+    }
+
+    @Benchmark
+    public static int fullJsonTemplateLayout4EcsLayout(
+            final JsonTemplateLayoutBenchmarkState state) {
+        return benchmark(
+                state.getJsonTemplateLayout4EcsLayout(),
+                state.getFullLogEvents(),
+                state.getByteBufferDestination());
+    }
+
+    @Benchmark
+    public static int liteJsonTemplateLayout4EcsLayout(
+            final JsonTemplateLayoutBenchmarkState state) {
+        return benchmark(
+                state.getJsonTemplateLayout4EcsLayout(),
+                state.getLiteLogEvents(),
+                state.getByteBufferDestination());
+    }
+
+    @Benchmark
+    public static int fullJsonTemplateLayout4GelfLayout(
+            final JsonTemplateLayoutBenchmarkState state) {
+        return benchmark(
+                state.getJsonTemplateLayout4GelfLayout(),
+                state.getFullLogEvents(),
+                state.getByteBufferDestination());
+    }
+
+    @Benchmark
+    public static int liteJsonTemplateLayout4GelfLayout(
+            final JsonTemplateLayoutBenchmarkState state) {
+        return benchmark(
+                state.getJsonTemplateLayout4GelfLayout(),
+                state.getLiteLogEvents(),
+                state.getByteBufferDestination());
+    }
+
+    @Benchmark
+    public static int fullDefaultJsonLayout(
+            final JsonTemplateLayoutBenchmarkState state) {
+        return benchmark(
+                state.getDefaultJsonLayout(),
+                state.getFullLogEvents(),
+                state.getByteBufferDestination());
+    }
+
+    @Benchmark
+    public static int liteDefaultJsonLayout(
+            final JsonTemplateLayoutBenchmarkState state) {
+        return benchmark(
+                state.getDefaultJsonLayout(),
+                state.getLiteLogEvents(),
+                state.getByteBufferDestination());
+    }
+
+    @Benchmark
+    public static int fullCustomJsonLayout(
+            final JsonTemplateLayoutBenchmarkState state) {
+        return benchmark(
+                state.getCustomJsonLayout(),
+                state.getFullLogEvents(),
+                state.getByteBufferDestination());
+    }
+
+    @Benchmark
+    public static int liteCustomJsonLayout(
+            final JsonTemplateLayoutBenchmarkState state) {
+        return benchmark(
+                state.getCustomJsonLayout(),
+                state.getLiteLogEvents(),
+                state.getByteBufferDestination());
+    }
+
+    @Benchmark
+    public static int fullEcsLayout(
+            final JsonTemplateLayoutBenchmarkState state) {
+        return benchmark(
+                state.getEcsLayout(),
+                state.getFullLogEvents(),
+                state.getByteBufferDestination());
+    }
+
+    @Benchmark
+    public static int liteEcsLayout(
+            final JsonTemplateLayoutBenchmarkState state) {
+        return benchmark(
+                state.getEcsLayout(),
+                state.getLiteLogEvents(),
+                state.getByteBufferDestination());
+    }
+
+    @Benchmark
+    public static int fullGelfLayout(
+            final JsonTemplateLayoutBenchmarkState state) {
+        return benchmark(
+                state.getGelfLayout(),
+                state.getFullLogEvents(),
+                state.getByteBufferDestination());
+    }
+
+    @Benchmark
+    public static int liteGelfLayout(
+            final JsonTemplateLayoutBenchmarkState state) {
+        return benchmark(
+                state.getGelfLayout(),
+                state.getLiteLogEvents(),
+                state.getByteBufferDestination());
+    }
+
+    private static int benchmark(
+            final Layout<String> layout,
+            final List<LogEvent> logEvents,
+            final ByteBufferDestination destination) {
+        // noinspection ForLoopReplaceableByForEach (avoid iterator instantiation)
+        for (int logEventIndex = 0; logEventIndex < logEvents.size(); logEventIndex++) {
+            LogEvent logEvent = logEvents.get(logEventIndex);
+            layout.encode(logEvent, destination);
+        }
+        final ByteBuffer byteBuffer = destination.getByteBuffer();
+        final int position = byteBuffer.position();
+        byteBuffer.clear();
+        return position;
+    }
+
+}
diff --git a/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkReport.java b/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkReport.java
new file mode 100644
index 0000000..2c43f0a
--- /dev/null
+++ b/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkReport.java
@@ -0,0 +1,359 @@
+package org.apache.logging.log4j.layout.json.template;
+
+import org.apache.logging.log4j.layout.json.template.util.JsonReader;
+import org.apache.logging.log4j.util.Strings;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.RoundingMode;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Utility class to summarize {@link JsonTemplateLayoutBenchmark} results in Asciidoctor.
+ * <p>
+ * Usage:
+ * <pre>
+ * java \
+ *     -cp log4j-perf/target/benchmarks.jar \
+ *     org.apache.logging.log4j.layout.json.template.JsonTemplateLayoutBenchmarkReport \
+ *     log4j-perf/target/JsonTemplateLayoutBenchmarkResult.json \
+ *     log4j-perf/target/JsonTemplateLayoutBenchmarkReport.adoc
+ * </pre>
+ * @see JsonTemplateLayoutBenchmark on how to generate JMH result JSON file
+ */
+public enum JsonTemplateLayoutBenchmarkReport {;
+
+    private static final Charset CHARSET = StandardCharsets.UTF_8;
+
+    public static void main(final String[] args) throws Exception {
+        final CliArgs cliArgs = CliArgs.parseArgs(args);
+        final JmhSetup jmhSetup = JmhSetup.ofJmhResult(cliArgs.jmhResultJsonFile);
+        final List<JmhSummary> jmhSummaries = JmhSummary.ofJmhResult(cliArgs.jmhResultJsonFile);
+        dumpReport(cliArgs.outputAdocFile, jmhSetup, jmhSummaries);
+    }
+
+    private static final class CliArgs {
+
+        private final File jmhResultJsonFile;
+
+        private final File outputAdocFile;
+
+        private CliArgs(final File jmhResultJsonFile, final File outputAdocFile) {
+            this.jmhResultJsonFile = jmhResultJsonFile;
+            this.outputAdocFile = outputAdocFile;
+        }
+
+        private static CliArgs parseArgs(final String[] args) {
+
+            // Check number of arguments.
+            if (args.length != 2) {
+                throw new IllegalArgumentException(
+                        "usage: <jmhResultJsonFile> <outputAdocFile>");
+            }
+
+            // Parse the JMH result JSON file.
+            final File jmhResultJsonFile = new File(args[0]);
+            if (!jmhResultJsonFile.isFile()) {
+                throw new IllegalArgumentException(
+                        "jmhResultJsonFile doesn't point to a regular file: " +
+                                jmhResultJsonFile);
+            }
+            if (!jmhResultJsonFile.canRead()) {
+                throw new IllegalArgumentException(
+                        "jmhResultJsonFile is not readable: " +
+                                jmhResultJsonFile);
+            }
+
+            // Parse the output AsciiDoc file.
+            final File outputAdocFile = new File(args[1]);
+            touch(outputAdocFile);
+
+            // Looks okay.
+            return new CliArgs(jmhResultJsonFile, outputAdocFile);
+
+        }
+
+        public static void touch(final File file) {
+            Objects.requireNonNull(file, "file");
+            final Path path = file.toPath();
+            try {
+                if (Files.exists(path)) {
+                    Files.setLastModifiedTime(path, FileTime.from(Instant.now()));
+                } else {
+                    Files.createFile(path);
+                }
+            } catch (IOException error) {
+                throw new RuntimeException("failed to touch file: " + file, error);
+            }
+        }
+
+    }
+
+    private static final class JmhSetup {
+
+        private final String vmName;
+
+        private final String vmVersion;
+
+        private final List<String> vmArgs;
+
+        private final int forkCount;
+
+        private final int warmupIterationCount;
+
+        private final String warmupTime;
+
+        private final int measurementIterationCount;
+
+        private final String measurementTime;
+
+        private JmhSetup(
+                final String vmName,
+                final String vmVersion,
+                final List<String> vmArgs,
+                final int forkCount,
+                final int warmupIterationCount,
+                final String warmupTime,
+                final int measurementIterationCount,
+                final String measurementTime) {
+            this.vmName = vmName;
+            this.vmVersion = vmVersion;
+            this.vmArgs = vmArgs;
+            this.forkCount = forkCount;
+            this.warmupIterationCount = warmupIterationCount;
+            this.warmupTime = warmupTime;
+            this.measurementIterationCount = measurementIterationCount;
+            this.measurementTime = measurementTime;
+        }
+
+        private static JmhSetup ofJmhResult(final File jmhResultFile) throws IOException {
+            final List<Object> jmhResult = readObject(jmhResultFile);
+            return ofJmhResult(jmhResult);
+        }
+
+        private static JmhSetup ofJmhResult(final List<Object> jmhResult) {
+            final Object jmhResultEntry = jmhResult.stream().findFirst().get();
+            final String vmName = readObjectAtPath(jmhResultEntry, "vmName");
+            final String vmVersion = readObjectAtPath(jmhResultEntry, "vmVersion");
+            final List<String> vmArgs = readObjectAtPath(jmhResultEntry, "jvmArgs");
+            final int forkCount = readObjectAtPath(jmhResultEntry, "forks");
+            final int warmupIterationCount = readObjectAtPath(jmhResultEntry, "warmupIterations");
+            final String warmupTime = readObjectAtPath(jmhResultEntry, "warmupTime");
+            final int measurementIterationCount = readObjectAtPath(jmhResultEntry, "measurementIterations");
+            final String measurementTime = readObjectAtPath(jmhResultEntry, "measurementTime");
+            return new JmhSetup(
+                    vmName,
+                    vmVersion,
+                    vmArgs,
+                    forkCount,
+                    warmupIterationCount,
+                    warmupTime,
+                    measurementIterationCount,
+                    measurementTime);
+        }
+
+    }
+
+    private static final class JmhSummary {
+
+        private final String benchmark;
+
+        private final BigDecimal opRate;
+
+        private final BigDecimal gcRate;
+
+        private JmhSummary(
+                final String benchmark,
+                final BigDecimal opRate,
+                final BigDecimal gcRate) {
+            this.benchmark = benchmark;
+            this.opRate = opRate;
+            this.gcRate = gcRate;
+        }
+
+        private static List<JmhSummary> ofJmhResult(final File jmhResultFile) throws IOException {
+            final List<Object> jmhResult = readObject(jmhResultFile);
+            return ofJmhResult(jmhResult);
+        }
+
+        private static List<JmhSummary> ofJmhResult(final List<Object> jmhResult) {
+            final BigDecimal maxOpRate = jmhResult
+                    .stream()
+                    .map(jmhResultEntry -> readBigDecimalAtPath(jmhResultEntry, "primaryMetric", "scorePercentiles", "99.0"))
+                    .max(BigDecimal::compareTo)
+                    .get();
+            return jmhResult
+                    .stream()
+                    .map(jmhResultEntry -> {
+                        final String benchmark = readObjectAtPath(jmhResultEntry, "benchmark");
+                        final BigDecimal opRate = readBigDecimalAtPath(jmhResultEntry, "primaryMetric", "scorePercentiles", "99.0");
+                        final BigDecimal gcRate = readBigDecimalAtPath(jmhResultEntry, "secondaryMetrics", "·gc.alloc.rate.norm", "scorePercentiles", "99.0");
+                        return new JmhSummary(benchmark, opRate, gcRate);
+                    })
+                    .collect(Collectors.toList());
+        }
+
+    }
+
+    private static <V> V readObject(final File file) throws IOException {
+        final byte[] jsonBytes = Files.readAllBytes(file.toPath());
+        final String json = new String(jsonBytes, CHARSET);
+        @SuppressWarnings("unchecked")
+        final V object = (V) JsonReader.read(json);
+        return object;
+    }
+
+    private static <V> V readObjectAtPath(final Object object, String... path) {
+        Object lastObject = object;
+        for (final String key : path) {
+            @SuppressWarnings("unchecked")
+            Map<String, Object> lastMap = (Map<String, Object>) lastObject;
+            lastObject = lastMap.get(key);
+        }
+        @SuppressWarnings("unchecked")
+        final V typedLastObject = (V) lastObject;
+        return typedLastObject;
+    }
+
+    private static BigDecimal readBigDecimalAtPath(final Object object, String... path) {
+        final Number number = readObjectAtPath(object, path);
+        if (number instanceof BigDecimal) {
+            return (BigDecimal) number;
+        } else if (number instanceof Integer) {
+            final int intNumber = (int) number;
+            return BigDecimal.valueOf(intNumber);
+        } else if (number instanceof Long) {
+            final long longNumber = (long) number;
+            return BigDecimal.valueOf(longNumber);
+        } else if (number instanceof BigInteger) {
+            final BigInteger bigInteger = (BigInteger) number;
+            return new BigDecimal(bigInteger);
+        } else {
+            final String message = String.format(
+                    "failed to convert the value to BigDecimal at path %s: %s",
+                    Arrays.asList(path), number);
+            throw new IllegalArgumentException(message);
+        }
+    }
+
+    private static void dumpReport(
+            final File outputAdocFile,
+            final JmhSetup jmhSetup,
+            final List<JmhSummary> jmhSummaries) throws IOException {
+        try (final OutputStream outputStream = new FileOutputStream(outputAdocFile);
+             final PrintStream printStream = new PrintStream(outputStream, false, CHARSET.name())) {
+            dumpJmhSetup(printStream, jmhSetup);
+            dumpJmhSummaries(printStream, jmhSummaries, "lite");
+            dumpJmhSummaries(printStream, jmhSummaries, "full");
+        }
+    }
+
+    private static void dumpJmhSetup(
+            final PrintStream printStream,
+            final JmhSetup jmhSetup) {
+        printStream.println("[cols=\"1,4\", options=\"header\"]");
+        printStream.println(".JMH setup");
+        printStream.println("|===");
+        printStream.println("|Setting|Value");
+        printStream.format("|JVM name|%s%n", jmhSetup.vmName);
+        printStream.format("|JVM version|%s%n", jmhSetup.vmVersion);
+        printStream.format("|JVM args|%s%n", jmhSetup.vmArgs != null ? String.join(" ", jmhSetup.vmArgs) : "");
+        printStream.format("|Forks|%s%n", jmhSetup.forkCount);
+        printStream.format("|Warmup iterations|%d × %s%n", jmhSetup.warmupIterationCount, jmhSetup.warmupTime);
+        printStream.format("|Measurement iterations|%d × %s%n", jmhSetup.measurementIterationCount, jmhSetup.measurementTime);
+        printStream.println("|===");
+    }
+
+    private static void dumpJmhSummaries(
+            final PrintStream printStream,
+            final List<JmhSummary> jmhSummaries,
+            final String prefix) {
+
+        // Print header.
+        printStream.println("[cols=\"4,>2,4,>2\", options=\"header\"]");
+        printStream.format(".JMH result (99^th^ percentile) summary for \"%s\" log events%n", prefix);
+        printStream.println("|===");
+        printStream.println("^|Benchmark");
+        printStream.println("2+^|ops/sec");
+        printStream.println("^|B/op");
+
+        // Filter JMH summaries by prefix.
+        final String filterRegex = String.format("^.*\\.%s[A-Za-z0-9]+$", prefix);
+        final List<JmhSummary> filteredJmhSummaries = jmhSummaries
+                .stream()
+                .filter(jmhSummary -> jmhSummary.benchmark.matches(filterRegex))
+                .collect(Collectors.toList());
+
+        // Determine the max. op rate.
+        final BigDecimal maxOpRate = filteredJmhSummaries
+                .stream()
+                .map(jmhSummary -> jmhSummary.opRate)
+                .max(BigDecimal::compareTo)
+                .get();
+
+        // Print each summary.
+        final Comparator<JmhSummary> jmhSummaryComparator =
+                Comparator
+                        .comparing((final JmhSummary jmhSummary) -> jmhSummary.opRate)
+                        .reversed();
+        filteredJmhSummaries
+                .stream()
+                .sorted(jmhSummaryComparator)
+                .forEach((final JmhSummary jmhSummary) -> {
+                    dumpJmhSummary(printStream, maxOpRate, jmhSummary);
+                });
+
+        // Print footer.
+        printStream.println("|===");
+
+    }
+
+    private static void dumpJmhSummary(
+            final PrintStream printStream,
+            final BigDecimal maxOpRate,
+            final JmhSummary jmhSummary) {
+        printStream.println();
+        final String benchmark = jmhSummary
+                .benchmark
+                .replaceAll("^.*\\.([^.]+)$", "$1");
+        printStream.format("|%s%n", benchmark);
+        final long opRatePerSec = jmhSummary
+                .opRate
+                .multiply(BigDecimal.valueOf(1_000L))
+                .toBigInteger()
+                .longValueExact();
+        printStream.format("|%,d%n", opRatePerSec);
+        final BigDecimal normalizedOpRate = jmhSummary
+                .opRate
+                .divide(maxOpRate, RoundingMode.CEILING);
+        final int opRateBarLength = normalizedOpRate
+                .multiply(BigDecimal.valueOf(19))
+                .toBigInteger()
+                .add(BigInteger.ONE)
+                .intValueExact();
+        final String opRateBar = Strings.repeat("▉", opRateBarLength);
+        final int opRatePercent = normalizedOpRate
+                .multiply(BigDecimal.valueOf(100))
+                .toBigInteger()
+                .intValueExact();
+        printStream.format("|%s (%d%%)%n", opRateBar, opRatePercent);
+        printStream.format("|%,.1f%n", jmhSummary.gcRate.doubleValue());
+    }
+
+}
diff --git a/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkState.java b/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkState.java
new file mode 100644
index 0000000..dc8dcd1
--- /dev/null
+++ b/log4j-perf/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutBenchmarkState.java
@@ -0,0 +1,212 @@
+/*
+ * 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 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.core.layout.ByteBufferDestination;
+import org.apache.logging.log4j.core.layout.GelfLayout;
+import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.core.util.NetUtils;
+import org.apache.logging.log4j.jackson.json.layout.JsonLayout;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalField;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayout.EventTemplateAdditionalFields;
+import org.apache.logging.log4j.layout.json.template.util.ThreadLocalRecyclerFactory;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+@State(Scope.Benchmark)
+public class JsonTemplateLayoutBenchmarkState {
+
+    private static final Configuration CONFIGURATION = new DefaultConfiguration();
+
+    private static final Charset CHARSET = StandardCharsets.UTF_8;
+
+    private final ByteBufferDestination byteBufferDestination;
+
+    private final JsonTemplateLayout jsonTemplateLayout4JsonLayout;
+
+    private final JsonTemplateLayout jsonTemplateLayout4EcsLayout;
+
+    private final JsonTemplateLayout jsonTemplateLayout4GelfLayout;
+
+    private final JsonLayout defaultJsonLayout;
+
+    private final JsonLayout customJsonLayout;
+
+    private final EcsLayout ecsLayout;
+
+    private final GelfLayout gelfLayout;
+
+    private final List<LogEvent> fullLogEvents;
+
+    private final List<LogEvent> liteLogEvents;
+
+    public JsonTemplateLayoutBenchmarkState() {
+        this.byteBufferDestination = new BlackHoleByteBufferDestination(1024 * 512);
+        this.jsonTemplateLayout4JsonLayout = createJsonTemplateLayout4JsonLayout();
+        this.jsonTemplateLayout4EcsLayout = createJsonTemplateLayout4EcsLayout();
+        this.jsonTemplateLayout4GelfLayout = createJsonTemplateLayout4GelfLayout();
+        this.defaultJsonLayout = createDefaultJsonLayout();
+        this.customJsonLayout = createCustomJsonLayout();
+        this.ecsLayout = createEcsLayout();
+        this.gelfLayout = createGelfLayout();
+        int logEventCount = 1_000;
+        this.fullLogEvents = LogEventFixture.createFullLogEvents(logEventCount);
+        this.liteLogEvents = LogEventFixture.createLiteLogEvents(logEventCount);
+    }
+
+    private static JsonTemplateLayout createJsonTemplateLayout4JsonLayout() {
+        return JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setCharset(CHARSET)
+                .setEventTemplateUri("classpath:JsonLayout.json")
+                .setRecyclerFactory(ThreadLocalRecyclerFactory.getInstance())
+                .build();
+    }
+
+    private static JsonTemplateLayout createJsonTemplateLayout4EcsLayout() {
+        final EventTemplateAdditionalFields additionalFields = EventTemplateAdditionalFields
+                .newBuilder()
+                .setAdditionalFields(
+                        new EventTemplateAdditionalField[]{
+                                EventTemplateAdditionalField
+                                        .newBuilder()
+                                        .setKey("service.name")
+                                        .setValue("benchmark")
+                                        .build()
+                        })
+                .build();
+        return JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setCharset(CHARSET)
+                .setEventTemplateUri("classpath:EcsLayout.json")
+                .setRecyclerFactory(ThreadLocalRecyclerFactory.getInstance())
+                .setEventTemplateAdditionalFields(additionalFields)
+                .build();
+    }
+
+    private static JsonTemplateLayout createJsonTemplateLayout4GelfLayout() {
+        return JsonTemplateLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setCharset(CHARSET)
+                .setEventTemplateUri("classpath:GelfLayout.json")
+                .setRecyclerFactory(ThreadLocalRecyclerFactory.getInstance())
+                .setEventTemplateAdditionalFields(EventTemplateAdditionalFields
+                        .newBuilder()
+                        .setAdditionalFields(
+                                new EventTemplateAdditionalField[]{
+                                        // Adding "host" as a constant rather than using
+                                        // the "hostName" property lookup at runtime, which
+                                        // is what GelfLayout does as well.
+                                        EventTemplateAdditionalField
+                                                .newBuilder()
+                                                .setKey("host")
+                                                .setValue(NetUtils.getLocalHostname())
+                                                .build()
+                                })
+                        .build())
+                .build();
+    }
+
+    private static JsonLayout createDefaultJsonLayout() {
+        return JsonLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setCharset(CHARSET)
+                .build();
+    }
+
+    private static JsonLayout createCustomJsonLayout() {
+        return JsonLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setCharset(CHARSET)
+                .setAdditionalFields(new KeyValuePair[]{
+                        new KeyValuePair("@version", "\"1\"")
+                })
+                .build();
+    }
+
+    private static EcsLayout createEcsLayout() {
+        return EcsLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setCharset(CHARSET)
+                .setServiceName("benchmark")
+                .build();
+    }
+
+    private static GelfLayout createGelfLayout() {
+        return GelfLayout
+                .newBuilder()
+                .setConfiguration(CONFIGURATION)
+                .setCharset(CHARSET)
+                .setCompressionType(GelfLayout.CompressionType.OFF)
+                .build();
+    }
+
+    ByteBufferDestination getByteBufferDestination() {
+        return byteBufferDestination;
+    }
+
+    JsonTemplateLayout getJsonTemplateLayout4JsonLayout() {
+        return jsonTemplateLayout4JsonLayout;
+    }
+
+    JsonTemplateLayout getJsonTemplateLayout4EcsLayout() {
+        return jsonTemplateLayout4EcsLayout;
+    }
+
+    JsonTemplateLayout getJsonTemplateLayout4GelfLayout() {
+        return jsonTemplateLayout4GelfLayout;
+    }
+
+    JsonLayout getDefaultJsonLayout() {
+        return defaultJsonLayout;
+    }
+
+    JsonLayout getCustomJsonLayout() {
+        return customJsonLayout;
+    }
+
+    EcsLayout getEcsLayout() {
+        return ecsLayout;
+    }
+
+    GelfLayout getGelfLayout() {
+        return gelfLayout;
+    }
+
+    List<LogEvent> getFullLogEvents() {
+        return fullLogEvents;
+    }
+
+    List<LogEvent> getLiteLogEvents() {
+        return liteLogEvents;
+    }
+
+}
diff --git a/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ClocksBenchmark.java b/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ClocksBenchmark.java
index ff778d2..2d2ccb3 100644
--- a/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ClocksBenchmark.java
+++ b/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ClocksBenchmark.java
@@ -152,7 +152,7 @@
         private static volatile OldCachedClock instance;
         private static final Object INSTANCE_LOCK = new Object();
         private volatile long millis = System.currentTimeMillis();
-        private volatile short count = 0;
+        private volatile short count;
 
         private OldCachedClock() {
             final Thread updater = new Thread(new Runnable() {
diff --git a/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/LoggerConfigBenchmark.java b/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/LoggerConfigBenchmark.java
index 0d36c21..41ef9c8 100644
--- a/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/LoggerConfigBenchmark.java
+++ b/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/LoggerConfigBenchmark.java
@@ -53,7 +53,7 @@
 public class LoggerConfigBenchmark {
 
     private final CopyOnWriteArraySet<AppenderControl> appenderSet = new CopyOnWriteArraySet<>();
-    private volatile Filter filter = null;
+    private volatile Filter filter;
     private final boolean additive = true;
     private final boolean includeLocation = true;
     private LoggerConfig parent;
diff --git a/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/StackWalkBenchmark.java b/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/StackWalkBenchmark.java
new file mode 100644
index 0000000..15dcfb6
--- /dev/null
+++ b/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/StackWalkBenchmark.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.perf.jmh;
+
+import org.apache.logging.log4j.perf.util.StackDriver;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.infra.Blackhole;
+
+import java.lang.StackWalker;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+/**
+ * Benchmark logging with logging disabled.
+ * // ============================== HOW TO RUN THIS TEST: ====================================
+ * //
+ * // single thread:
+ * // java -jar log4j-perf/target/benchmarks.jar ".*StackWalkBenchmark.*" -f 1 -wi 5 -i 10
+ * //
+ * // multiple threads (for example, 4 threads):
+ * // java -jar log4j-perf/target/benchmarks.jar ".*StackWalkBenchmark.*" -f 1 -wi 5 -i 10 -t 4 -si true
+ * //
+ * // Usage help:
+ * // java -jar log4j-perf/target/benchmarks.jar -help
+ * //
+ */
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class StackWalkBenchmark {
+
+    private static final StackDriver stackDriver = new StackDriver();
+    private final static ThreadLocal<String> FQCN = new ThreadLocal<>();
+    private final static FqcnCallerLocator LOCATOR = new FqcnCallerLocator();
+    private final static StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);
+
+    @Param({"10", "20", "50"})
+    private int initialDepth;
+
+    @Param({"5", "10", "20"})
+    private int callDepth;
+
+    @Benchmark
+    public void throwableSearch(Blackhole bh)  {
+
+        stackDriver.deepCall(initialDepth, callDepth, (fqcn) -> {
+            final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
+            boolean found = false;
+            for (int i = 0; i < stackTrace.length; i++) {
+                final String className = stackTrace[i].getClassName();
+                if (fqcn.equals(className)) {
+                    found = true;
+                    continue;
+                }
+                if (found  && !fqcn.equals(className)) {
+                    return stackTrace[i];
+                }
+            }
+            return null;
+        });
+    }
+
+    @Benchmark
+    public void stackWalkerWalk(Blackhole bh) {
+        stackDriver.deepCall(initialDepth, callDepth, (fqcn) -> {
+            return walker.walk(
+                    s -> s.dropWhile(f -> !f.getClassName().equals(fqcn)) // drop the top frames until we reach the logger
+                            .dropWhile(f -> f.getClassName().equals(fqcn)) // drop the logger frames
+                            .findFirst())
+                    .get()
+                    .toStackTraceElement();
+        });
+    }
+
+    @Benchmark
+    public void baseline(Blackhole bh)  {
+
+        stackDriver.deepCall(initialDepth, callDepth, (fqcn) -> {
+            return null;
+        });
+    }
+
+    @Benchmark
+    public void stackWalkerArray(Blackhole bh)  {
+
+        stackDriver.deepCall(initialDepth, callDepth, (fqcn) -> {
+            FQCN.set(fqcn);
+            final StackWalker.StackFrame walk = walker.walk(LOCATOR);
+            final StackTraceElement element = walk == null ? null : walk.toStackTraceElement();
+            FQCN.set(null);
+            return element;
+        });
+    }
+
+    static final class FqcnCallerLocator implements Function<Stream<StackWalker.StackFrame>, StackWalker.StackFrame> {
+
+        @Override
+        public StackWalker.StackFrame apply(Stream<StackWalker.StackFrame> stackFrameStream) {
+            String fqcn = FQCN.get();
+            boolean foundFqcn = false;
+            Object[] frames = stackFrameStream.toArray();
+            for (int i = 0; i < frames.length ; ++i) {
+                final String className = ((StackWalker.StackFrame) frames[i]).getClassName();
+                if (!foundFqcn) {
+                    // Skip frames until we find the FQCN
+                    foundFqcn = className.equals(fqcn);
+                } else if (!className.equals(fqcn)) {
+                    // The frame is no longer equal to the FQCN so it is the one we want.
+                    return (StackWalker.StackFrame) frames[i];
+                } // Otherwise it is equal to the FQCN so we need to skip it.
+            }
+            // Should never happen
+            return null;
+        }
+    }
+
+}
diff --git a/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadLocalVsPoolBenchmark.java b/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadLocalVsPoolBenchmark.java
index beb1456..2996dec 100644
--- a/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadLocalVsPoolBenchmark.java
+++ b/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadLocalVsPoolBenchmark.java
@@ -17,14 +17,7 @@
 
 package org.apache.logging.log4j.perf.jmh;
 
-import java.nio.charset.Charset;
-import java.util.Deque;
-import java.util.List;
-import java.util.concurrent.ConcurrentLinkedDeque;
-
 import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.Marker;
-import org.apache.logging.log4j.ThreadContext.ContextStack;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.config.Configuration;
 import org.apache.logging.log4j.core.config.DefaultConfiguration;
@@ -34,14 +27,16 @@
 import org.apache.logging.log4j.core.pattern.PatternParser;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.SimpleMessage;
-import org.apache.logging.log4j.util.StringMap;
+import org.jctools.queues.MpmcArrayQueue;
 import org.openjdk.jmh.annotations.Benchmark;
 import org.openjdk.jmh.annotations.Scope;
 import org.openjdk.jmh.annotations.State;
 
+import java.util.List;
+
 /**
- * Checks PatternLayout performance when reusing the StringBuilder in a ThreadLocal, an ObjectPool or when creating a
- * new instance for each log event.
+ * Checks {@link PatternFormatter} performance with various StringBuilder
+ * caching strategies: no-op, ThreadLocal, and JCTools MPMC queue.
  */
 // ============================== HOW TO RUN THIS TEST: ====================================
 // (Quick build: mvn -DskipTests=true clean package -pl log4j-perf -am )
@@ -58,160 +53,159 @@
 @State(Scope.Benchmark)
 public class ThreadLocalVsPoolBenchmark {
 
-    static final Charset CHARSET_DEFAULT = Charset.defaultCharset();
-    static final String LOG4JPATTERN = "%d %5p [%t] %c{1} %X{transactionId} - %m%n";
-    static final int DEFAULT_STRING_BUILDER_SIZE = 1024;
+    private static final LogEvent LOG_EVENT = createLogEvent();
 
-    /**
-     * The LogEvent to serialize.
-     */
-    private final static LogEvent LOG4J2EVENT = createLog4j2Event();
+    private static final List<PatternFormatter> FORMATTERS = createFormatters();
 
-    /**
-     * Initial converter for pattern.
-     */
-    private final static PatternFormatter[] formatters = createFormatters();
-
-    private final StringBuilderPool pool = new StringBuilderPool(DEFAULT_STRING_BUILDER_SIZE);
-    private static ThreadLocal<StringBuilder> threadLocal = new ThreadLocal<>();
-
-    /**
-     */
-    private static PatternFormatter[] createFormatters() {
-        final Configuration config = new DefaultConfiguration();
-        final PatternParser parser = new PatternParser(config, "Converter", LogEventPatternConverter.class);
-        final List<PatternFormatter> result = parser.parse(LOG4JPATTERN, false, true);
-        return result.toArray(new PatternFormatter[result.size()]);
-    }
-
-    @Benchmark
-    public byte[] newInstance() {
-        return serializeWithNewInstance(LOG4J2EVENT).getBytes(CHARSET_DEFAULT);
-    }
-
-    @Benchmark
-    public byte[] threadLocal() {
-        return serializeWithThreadLocal(LOG4J2EVENT).getBytes(CHARSET_DEFAULT);
-    }
-
-    @Benchmark
-    public byte[] objectPool() {
-        return serializeWithPool(LOG4J2EVENT).getBytes(CHARSET_DEFAULT);
-    }
-
-    @Benchmark
-    public String _stringNewInstance() {
-        return serializeWithNewInstance(LOG4J2EVENT);
-    }
-
-    @Benchmark
-    public String _stringThreadLocal() {
-        return serializeWithThreadLocal(LOG4J2EVENT);
-    }
-
-    @Benchmark
-    public String _stringObjectPool() {
-        return serializeWithPool(LOG4J2EVENT);
-    }
-
-    private String serializeWithNewInstance(final LogEvent event) {
-        final StringBuilder buf = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
-        return serialize(event, buf);
-    }
-
-    private String serializeWithThreadLocal(final LogEvent event) {
-        StringBuilder buf = threadLocal.get();
-        if (buf == null) {
-            buf = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
-            threadLocal.set(buf);
-        }
-        buf.setLength(0);
-        return serialize(event, buf);
-    }
-
-    private String serializeWithPool(final LogEvent event) {
-        final StringBuilder buf = pool.borrowObject();
-        try {
-            buf.setLength(0);
-            return serialize(event, buf);
-        } finally {
-            pool.returnObject(buf);
-        }
-    }
-
-    private String serialize(final LogEvent event, final StringBuilder buf) {
-        final int len = formatters.length;
-        for (int i = 0; i < len; i++) {
-            formatters[i].format(event, buf);
-        }
-        return buf.toString();
-    }
-
-    private static LogEvent createLog4j2Event() {
-        final Marker marker = null;
-        final String fqcn = "com.mycom.myproject.mypackage.MyClass";
+    private static LogEvent createLogEvent() {
+        final String loggerName = "name(ignored)";
+        final String loggerFqcn = "com.mycom.myproject.mypackage.MyClass";
         final Level level = Level.DEBUG;
-        final String STR = "AB!(%087936DZYXQWEIOP$#^~-=/><nb"; // length=32
-        final Message message = new SimpleMessage(STR);
-        final Throwable t = null;
-        final StringMap mdc = null;
-        final ContextStack ndc = null;
-        final String threadName = null;
-        final StackTraceElement location = null;
+        final String messageString = "AB!(%087936DZYXQWEIOP$#^~-=/><nb"; // length=32
+        final Message message = new SimpleMessage(messageString);
         final long timestamp = 12345678;
-
-        return Log4jLogEvent.newBuilder() //
-                .setLoggerName("name(ignored)") //
-                .setMarker(marker) //
-                .setLoggerFqcn(fqcn) //
-                .setLevel(level) //
-                .setMessage(message) //
-                .setThrown(t) //
-                .setContextData(mdc) //
-                .setContextStack(ndc) //
-                .setThreadName(threadName) //
-                .setSource(location) //
-                .setTimeMillis(timestamp) //
+        return Log4jLogEvent
+                .newBuilder()
+                .setLoggerName(loggerName)
+                .setLoggerFqcn(loggerFqcn)
+                .setLevel(level)
+                .setMessage(message)
+                .setTimeMillis(timestamp)
                 .build();
     }
-}
 
-/**
- * 
- */
-abstract class ObjectPool<T> {
-    private final Deque<T> pool = new ConcurrentLinkedDeque<>();
-
-    public T borrowObject() {
-        final T object = pool.poll();
-        return object == null ? createObject() : object;
+    private static List<PatternFormatter> createFormatters() {
+        final Configuration config = new DefaultConfiguration();
+        final PatternParser parser = new PatternParser(config, "Converter", LogEventPatternConverter.class);
+        return parser.parse("%d %5p [%t] %c{1} %X{transactionId} - %m%n", false, true);
     }
 
-    public void returnObject(final T object) {
-        pool.add(object);
+    private static abstract class StringBuilderPool {
+
+        abstract StringBuilder acquire();
+
+        abstract void release(StringBuilder stringBuilder);
+
+        StringBuilder createStringBuilder() {
+            return new StringBuilder(1024 * 32);
+        }
+
     }
 
-    protected abstract T createObject();
-}
+    private static final class AllocatePool extends StringBuilderPool {
 
-/**
- * 
- */
-class StringBuilderPool extends ObjectPool<StringBuilder> {
-    private final int initialSize;
+        private static final AllocatePool INSTANCE = new AllocatePool();
 
-    public StringBuilderPool(final int stringBuilderSize) {
-        this.initialSize = stringBuilderSize;
+        @Override
+        public StringBuilder acquire() {
+            return createStringBuilder();
+        }
+
+        @Override
+        public void release(final StringBuilder stringBuilder) {}
+
     }
 
-    @Override
-    public void returnObject(final StringBuilder stringBuilder) {
-        stringBuilder.setLength(0);
-        super.returnObject(stringBuilder);
+    private static final class ThreadLocalPool extends StringBuilderPool {
+
+        private static final ThreadLocalPool INSTANCE = new ThreadLocalPool();
+
+        private final ThreadLocal<StringBuilder> stringBuilderRef =
+                ThreadLocal.withInitial(this::createStringBuilder);
+
+        @Override
+        public StringBuilder acquire() {
+            return stringBuilderRef.get();
+        }
+
+        @Override
+        public void release(final StringBuilder stringBuilder) {
+            stringBuilder.setLength(0);
+        }
+
     }
 
-    @Override
-    protected StringBuilder createObject() {
-        return new StringBuilder(initialSize);
+    private static final class JcPool extends StringBuilderPool {
+
+        private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
+
+        private static final int MPMC_REQUIRED_MIN_CAPACITY = 2;
+
+        // Putting the under-provisioned instance to a wrapper class to prevent
+        // the initialization of JcPool itself when there are insufficient CPU
+        // cores.
+        private enum UnderProvisionedInstanceHolder {;
+
+            private static final JcPool INSTANCE = createInstance();
+
+            private static JcPool createInstance() {
+                if (CPU_COUNT <= MPMC_REQUIRED_MIN_CAPACITY) {
+                    throw new IllegalArgumentException("insufficient CPU cores");
+                }
+                return new JcPool(MPMC_REQUIRED_MIN_CAPACITY);
+            }
+
+        }
+
+        private static final JcPool RIGHT_PROVISIONED_INSTANCE =
+                new JcPool(Math.max(MPMC_REQUIRED_MIN_CAPACITY, CPU_COUNT));
+
+        private final MpmcArrayQueue<StringBuilder> stringBuilders;
+
+        private JcPool(final int capacity) {
+            this.stringBuilders = new MpmcArrayQueue<>(capacity);
+        }
+
+        @Override
+        public StringBuilder acquire() {
+            final StringBuilder stringBuilder = stringBuilders.poll();
+            return stringBuilder != null
+                    ? stringBuilder
+                    : createStringBuilder();
+        }
+
+        @Override
+        public void release(final StringBuilder stringBuilder) {
+            stringBuilder.setLength(0);
+            stringBuilders.offer(stringBuilder);
+        }
+
     }
+
+    @Benchmark
+    public int allocate() {
+        return findSerializedLength(AllocatePool.INSTANCE);
+    }
+
+    @Benchmark
+    public int threadLocal() {
+        return findSerializedLength(ThreadLocalPool.INSTANCE);
+    }
+
+    @Benchmark
+    public int rightProvedJc() {
+        return findSerializedLength(JcPool.RIGHT_PROVISIONED_INSTANCE);
+    }
+
+    @Benchmark
+    public int underProvedJc() {
+        return findSerializedLength(JcPool.UnderProvisionedInstanceHolder.INSTANCE);
+    }
+
+    private int findSerializedLength(final StringBuilderPool pool) {
+        final StringBuilder stringBuilder = pool.acquire();
+        serialize(stringBuilder);
+        final int length = stringBuilder.length();
+        pool.release(stringBuilder);
+        return length;
+    }
+
+    private void serialize(final StringBuilder stringBuilder) {
+        // noinspection ForLoopReplaceableByForEach (avoid iterator instantiation)
+        for (int formatterIndex = 0; formatterIndex < FORMATTERS.size(); formatterIndex++) {
+            PatternFormatter formatter = FORMATTERS.get(formatterIndex);
+            formatter.format(LOG_EVENT, stringBuilder);
+        }
+    }
+
 }
diff --git a/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java b/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java
index fd462c2..b82547c 100644
--- a/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java
+++ b/log4j-perf/src/main/java/org/apache/logging/log4j/perf/jmh/TimeFormatBenchmark.java
@@ -57,8 +57,8 @@
     };
     FastDateFormat fastDateFormat = FastDateFormat.getInstance("HH:mm:ss.SSS");
     FixedDateFormat fixedDateFormat = FixedDateFormat.createIfSupported(new String[]{"ABSOLUTE"});
-    volatile long midnightToday = 0;
-    volatile long midnightTomorrow = 0;
+    volatile long midnightToday;
+    volatile long midnightTomorrow;
 
     @State(Scope.Thread)
     public static class BufferState {
diff --git a/log4j-perf/src/main/java/org/apache/logging/log4j/perf/util/StackDriver.java b/log4j-perf/src/main/java/org/apache/logging/log4j/perf/util/StackDriver.java
new file mode 100644
index 0000000..9f239e4
--- /dev/null
+++ b/log4j-perf/src/main/java/org/apache/logging/log4j/perf/util/StackDriver.java
@@ -0,0 +1,47 @@
+/*
+ * 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.perf.util;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+/**
+ * Facilitates creating a Call Stack for testing the performance of walkign it.
+ */
+public class StackDriver {
+    public StackTraceElement deepCall(int initialDepth, Integer targetDepth, Function<String, StackTraceElement> supplier) {
+        if (--initialDepth == 0) {
+            Processor processor = new Processor();
+            return processor.apply(targetDepth, supplier);
+        } else {
+            return deepCall(initialDepth, targetDepth, supplier);
+        }
+    }
+
+    public static class Processor implements BiFunction<Integer, Function<String, StackTraceElement>, StackTraceElement> {
+        private static final String FQCN = Processor.class.getName();
+
+        @Override
+        public StackTraceElement apply(Integer depth, Function<String, StackTraceElement> function) {
+            if (--depth == 0) {
+                return function.apply(FQCN);
+            } else {
+                return apply(depth, function);
+            }
+        }
+    }
+}
diff --git a/log4j-samples/log4j-samples-flume-common/src/main/java/org/apache/logging/log4j/samples/app/LoggingController.java b/log4j-samples/log4j-samples-flume-common/src/main/java/org/apache/logging/log4j/samples/app/LoggingController.java
index e77650a..02d47b3 100755
--- a/log4j-samples/log4j-samples-flume-common/src/main/java/org/apache/logging/log4j/samples/app/LoggingController.java
+++ b/log4j-samples/log4j-samples-flume-common/src/main/java/org/apache/logging/log4j/samples/app/LoggingController.java
@@ -45,7 +45,7 @@
      */
     private static Logger logger = LogManager.getLogger(LoggingController.class);
 
-    private volatile boolean generateLog = false;
+    private volatile boolean generateLog;
     private final Random ran = new Random();
 
     private List<AuditEvent> events;
diff --git a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/docker/app-compose.yml b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/docker/app-compose.yml
index 01cb293..9864e8e 100755
--- a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/docker/app-compose.yml
+++ b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/docker/app-compose.yml
@@ -8,7 +8,7 @@
       SERVICE_PARAMS: --spring.config.location=classpath:/,classpath:/application-local-docker.yml
     ports:
       - "5005:5005"
-      - "8080:4567"
+      - "8080:8080"
     networks:
       sample_network:
         aliases:
diff --git a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/docker/combined-compose.yml b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/docker/combined-compose.yml
new file mode 100755
index 0000000..b2f5abb
--- /dev/null
+++ b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/docker/combined-compose.yml
@@ -0,0 +1,86 @@
+version: "3"
+services:
+  socat:
+    container_name: socat
+    image: bobrik/socat
+    command: TCP-LISTEN:1234,fork UNIX-CONNECT:/var/run/docker.sock
+    expose:
+      - "1234"
+    volumes:
+      - /var/run/docker.sock:/var/run/docker.sock
+    networks:
+      sample_network:
+        aliases:
+          - socat
+
+  rabbitmq:
+    container_name: rabbit
+    image: rabbitmq:3-management-alpine
+    expose:
+      - "5672"
+      - "15672"
+    ports:
+      - "5672:5672"
+      - "15672:15672"
+    volumes:
+      - ./init/rabbit/rabbitmq.config:/etc/rabbitmq/rabbitmq.config:ro
+      - ./init/rabbit/definitions.json:/etc/rabbitmq/definitions.json:ro
+    networks:
+      sample_network:
+        aliases:
+          - rabbitmq
+
+  fluent-bit:
+    container_name: fluent-bit
+    image: fluent/fluent-bit:latest
+    expose:
+      - "2020"
+      - "24221"
+      - "24224"
+    ports:
+      - "24224:24224"
+    volumes:
+      - ./init/fluent-bit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf
+      - ./target/logs:/var/logs
+    networks:
+      sample_network:
+        aliases:
+          - fluent-bit
+
+  flume:
+    container_name: flume
+    image: probablyfine/flume:latest
+    expose:
+      - "5050"
+    environment:
+      FLUME_AGENT_NAME: forwarder
+      FLUME_JAVA_OPTS: -Dlog4j.configuration=file:///opt/flume-config/log4j.properties
+    volumes:
+      - ./init/flume/start-flume.sh:/opt/flume/bin/start-flume
+      - ./init/flume/flume.conf:/opt/flume-config/flume.conf
+      - ./init/flume/flume-env.sh:/opt/flume-config/flume-env.sh
+      - ./init/flume/log4j.properties:/opt/flume-config/log4j.properties
+      - ~/flume-logs:/var/log/flume
+    networks:
+      sample_network:
+        aliases:
+          - flume
+
+  sampleapp:
+    container_name: sampleapp
+    image: sampleapp
+    environment:
+      DOCKER_URI: http://socat:1234
+      SERVICE_PARAMS: --spring.config.location=classpath:/,classpath:/application-local-docker.yml
+    ports:
+      - "5005:5005"
+      - "8080:8080"
+    networks:
+      sample_network:
+        aliases:
+          - sampleapp
+networks:
+  sample_network:
+
+volumes:
+  pgdata:
\ No newline at end of file
diff --git a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/docker/restartApp.sh b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/docker/restartApp.sh
index 4178b12..af20bf4 100755
--- a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/docker/restartApp.sh
+++ b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/docker/restartApp.sh
@@ -4,7 +4,7 @@
 networkName=docker_sample_network
 debug_port=5005
 #debug_expose="-p $debug_port:$debug_port"
-exposed_ports="-p 8080:4567 $debug_expose"
+exposed_ports="-p 8080:8080 $debug_expose"
 
 mvn clean package -DskipTests=true
 
@@ -15,6 +15,6 @@
 
 echo Run new container...
 docker run  -e "SERVICE_PARAMS=--spring.config.location=classpath:/,classpath:/application-local-docker.yml" \
-    -e "DOCKER_URI=http://socat:1234" \
+    -e "DOCKER_URI=http://socat:1234" -e "JAVA_OPTS=-Dlogstash.search.host=host.docker.internal" \
     --network=$networkName -d $exposed_ports --name $containerName -h sample $imageName
 #    --log-driver=fluentd --log-opt fluentd-address=host.docker.internal:24224 \
\ No newline at end of file
diff --git a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/k8s/sampleapp-deployment.yaml b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/k8s/sampleapp-deployment.yaml
index fe5d2b7..ffd2851 100644
--- a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/k8s/sampleapp-deployment.yaml
+++ b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/k8s/sampleapp-deployment.yaml
@@ -15,30 +15,39 @@
         app: sampleapp
     spec:
       containers:
-        - name: sampleapp
-          image: localhost:5000/sampleapp:latest
-          imagePullPolicy: Always
-          ports:
-            - containerPort: 8080
-            - containerPort: 5005
-          env:
-            - name: JAVA_OPTS
-              value: "-Delastic.search.host=host.docker.internal"
+      - name: sampleapp
+        image: localhost:5000/sampleapp:latest
+        imagePullPolicy: Always
+        ports:
+          - containerPort: 8080
+          - containerPort: 5005
+        env:
+          - name: JAVA_OPTS
+            value: "-Delastic.search.host=host.docker.internal"
+      - name: key-value-store
+        image: redis
+        ports:
+        - containerPort: 6379
+
+
 ---
 apiVersion: v1
 kind: Service
 metadata:
   name: sampleapp
 spec:
-  type: NodePort
   selector:
     app: sampleapp
   ports:
     - protocol: TCP
       port: 8080
-      nodePort: 30011
+      targetPort: 8080
       name: http
     - protocol: TCP
+      port: 6379
+      targetPort: 6379
+      name: redis
+    - protocol: TCP
       port: 5005
-      nodePort: 30012
+      targetPort: 5005
       name: debug
diff --git a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/pom.xml b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/pom.xml
index 42b43b4..95a02fc 100644
--- a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/pom.xml
+++ b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/pom.xml
@@ -94,6 +94,11 @@
     </dependency>
     <dependency>
       <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-layout-json-template</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-kubernetes</artifactId>
       <version>${project.version}</version>
     </dependency>
@@ -172,7 +177,7 @@
         <configuration>
           <toolchains>
             <jdk>
-              <version>[8, )</version>
+              <version>1.8</version>
             </jdk>
           </toolchains>
         </configuration>
diff --git a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/src/main/java/org/apache/logging/log4j/spring/cloud/config/sample/controller/K8SController.java b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/src/main/java/org/apache/logging/log4j/spring/cloud/config/sample/controller/K8SController.java
new file mode 100644
index 0000000..5059a49
--- /dev/null
+++ b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/src/main/java/org/apache/logging/log4j/spring/cloud/config/sample/controller/K8SController.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2020 Nextiva, Inc. to Present.
+ * All rights reserved.
+ */
+
+package org.apache.logging.log4j.spring.cloud.config.sample.controller;
+
+import java.nio.file.Paths;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.kubernetes.KubernetesClientBuilder;
+import org.apache.logging.log4j.util.Strings;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import io.fabric8.kubernetes.api.model.Pod;
+import io.fabric8.kubernetes.client.Config;
+import io.fabric8.kubernetes.client.KubernetesClient;
+
+/**
+ * Test class
+ */
+@RestController
+public class K8SController {
+
+    private static final Logger LOGGER = LogManager.getLogger(K8SController.class);
+    private static final String HOSTNAME = "HOSTNAME";
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @GetMapping("/k8s/pod")
+    public ResponseEntity<Pod> getPod() {
+        try {
+            KubernetesClient client = new KubernetesClientBuilder().createClient();
+            if (client != null) {
+                Pod pod = getCurrentPod(client);
+                if (pod != null) {
+                    LOGGER.info("Pod: {}", objectMapper.writeValueAsString(pod));
+                    return new ResponseEntity<>(pod, HttpStatus.OK);
+                }
+            }
+        } catch (Exception ex) {
+            LOGGER.error("Unable to obtain or print Pod information", ex);
+        }
+        return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+    }
+
+    private Pod getCurrentPod(KubernetesClient kubernetesClient) {
+        String hostName = System.getenv(HOSTNAME);
+        try {
+            if (isServiceAccount() && Strings.isNotBlank(hostName)) {
+                return kubernetesClient.pods().withName(hostName).get();
+            }
+        } catch (Throwable t) {
+            LOGGER.debug("Unable to locate pod with name {}.", hostName);
+        }
+        return null;
+    }
+
+    private boolean isServiceAccount() {
+        return Paths.get(Config.KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH).toFile().exists()
+                && Paths.get(Config.KUBERNETES_SERVICE_ACCOUNT_CA_CRT_PATH).toFile().exists();
+    }
+
+}
diff --git a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/src/main/java/org/apache/logging/log4j/spring/cloud/config/sample/controller/SampleController.java b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/src/main/java/org/apache/logging/log4j/spring/cloud/config/sample/controller/SampleController.java
index 3d47218..7657503 100644
--- a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/src/main/java/org/apache/logging/log4j/spring/cloud/config/sample/controller/SampleController.java
+++ b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/src/main/java/org/apache/logging/log4j/spring/cloud/config/sample/controller/SampleController.java
@@ -25,6 +25,8 @@
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.util.UuidUtil;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
@@ -52,6 +54,7 @@
         }
         String msg = "";
         if (threads == 1) {
+            ThreadContext.put("requestId", UuidUtil.getTimeBasedUuid().toString());
             Timer timer = new Timer("sample");
             timer.start();
             for (int n = 0; n < count; ++n) {
@@ -61,6 +64,7 @@
             StringBuilder sb = new StringBuilder("Elapsed time = ");
             timer.formatTo(sb);
             msg = sb.toString();
+            ThreadContext.clearMap();
         } else {
             ExecutorService service = Executors.newFixedThreadPool(threads);
             Timer timer = new Timer("sample");
@@ -85,6 +89,7 @@
 
     @GetMapping("/exception")
     public ResponseEntity<String> exception() {
+        ThreadContext.put("requestId", UuidUtil.getTimeBasedUuid().toString());
         Throwable t = new Throwable("This is a test");
         LOGGER.info("This is a test", t);
         ByteArrayOutputStream os = new ByteArrayOutputStream();
@@ -92,6 +97,7 @@
         t.printStackTrace(ps);
         String stackTrace = os.toString();
         stackTrace = stackTrace.replaceAll("\n", "<br>");
+        ThreadContext.clearMap();
 
         //LOGGER.info("Hello, World");
         return ResponseEntity.ok(stackTrace);
@@ -109,9 +115,11 @@
 
         public void run() {
             String prefix = "Thread " + id + " record ";
+            ThreadContext.put("requestId", UuidUtil.getTimeBasedUuid().toString());
             for (int i = 0; i < count; ++i) {
                 LOGGER.info(prefix + i);
             }
+            ThreadContext.clearMap();
         }
     }
 }
diff --git a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/src/main/resources/EnhancedGelf.json b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/src/main/resources/EnhancedGelf.json
new file mode 100644
index 0000000..994b0c7
--- /dev/null
+++ b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/src/main/resources/EnhancedGelf.json
@@ -0,0 +1,41 @@
+{
+  "version": "1.1",
+  "host": "${hostName}",
+  "short_message": {
+    "$resolver": "message",
+    "stringified": true
+  },
+  "full_message": {
+    "$resolver": "message",
+    "pattern": "[%t] %-5p %X{requestId, sessionId, loginId, userId, ipAddress, corpAcctNumber} %C{1.}.%M:%L - %m",
+    "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-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/src/main/resources/application.yml b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/src/main/resources/application.yml
index 7e9838e..4c6bb12 100644
--- a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/src/main/resources/application.yml
+++ b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-application/src/main/resources/application.yml
@@ -1,5 +1,5 @@
 server:
-  port: 4567
+  port: 8080
   servlet:
     context-path: /sample
 
diff --git a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-server/src/main/config-repo/log4j2.xml b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-server/src/main/config-repo/log4j2.xml
index 2e9dcfa..a2afa70 100644
--- a/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-server/src/main/config-repo/log4j2.xml
+++ b/log4j-spring-cloud-config/log4j-spring-cloud-config-samples/log4j-spring-cloud-config-sample-server/src/main/config-repo/log4j2.xml
@@ -101,11 +101,37 @@
       <SizeBasedTriggeringPolicy size="10MB" />
       <DefaultRolloverStrategy max="5"/>
     </RollingFile>-->
-    <Socket name="Elastic" host="\${sys:elastic.search.host:-localhost}" port="12222" protocol="tcp" bufferedIo="true" ignoreExceptions="false">
+    <Socket name="Elastic"
+            host="\${sys:logstash.search.host}"
+            port="12222"
+            protocol="tcp"
+            bufferedIo="true">
+      <JsonTemplateLayout eventTemplateUri="classpath:EnhancedGelf.json" nullEventDelimiterEnabled="true">
+        <EventTemplateAdditionalFields>
+          <EventTemplateAdditionalField key="containerId" value="\${docker:containerId:-}"/>
+          <EventTemplateAdditionalField key="application" value="\${lower:${spring:spring.application.name:-spring}}"/>
+          <EventTemplateAdditionalField key="kubernetes.serviceAccountName" value="\${k8s:accountName:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.containerId" value="\${k8s:containerId:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.containerName" value="\${k8s:containerName:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.host" value="\${k8s:host:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.labels.app" value="\${k8s:labels.app:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.labels.pod-template-hash" value="\${k8s:labels.podTemplateHash:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.master_url" value="\${k8s:masterUrl:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.namespaceId" value="\${k8s:namespaceId:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.namespaceName" value="\${k8s:namespaceName:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.podID" value="\${k8s:podId:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.podIP" value="\${k8s:podIp:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.podName" value="\${k8s:podName:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.imageId" value="\${k8s:imageId:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.imageName" value="\${k8s:imageName:-}"/>
+        </EventTemplateAdditionalFields>
+      </JsonTemplateLayout>
+    </Socket>
+    <!--<Socket name="Elastic" host="\${sys:logstash.search.host:-localhost}" port="12222" protocol="tcp" bufferedIo="true" ignoreExceptions="false">
       <GelfLayout includeStackTrace="true" host="${hostName}" includeThreadContext="true" includeNullDelimiter="true"
                   compressionType="OFF">
         <ThreadContextIncludes>requestId,sessionId,loginId,userId,ipAddress,corpAcctNumber,callingHost,ohBehalfOf,onBehalfOfAccount</ThreadContextIncludes>
-        <MessagePattern>%d [%t] %-5p %X{requestId, sessionId, loginId, userId, ipAddress, corpAcctNumber} %C{1.}.%M:%L - %m%n</MessagePattern>
+        <MessagePattern>[%t] %-5p %X{requestId, sessionId, loginId, userId, ipAddress, corpAcctNumber} %C{1.}.%M:%L - %m%n</MessagePattern>
         <KeyValuePair key="docker.containerId" value="\${docker:containerId:-}"/>
         <KeyValuePair key="application" value="$\${lower:\${spring:spring.application.name}}"/>
         <KeyValuePair key="kubernetes.serviceAccountName" value="\${k8s:accountName:-}"/>
@@ -123,7 +149,7 @@
         <KeyValuePair key="kubernetes.imageId" value="\${k8s:imageId:-}"/>
         <KeyValuePair key="kubernetes.imageName" value="\${k8s:imageName:-}"/>
       </GelfLayout>
-    </Socket>
+    </Socket>-->
     <Console name="Console" target="SYSTEM_OUT">
       <RFC5424Layout enterpriseNumber="50177" includeMDC="true" mdcId="RequestContext" appName="SalesforceGateway"
                      mdcPrefix="" newLine="true" mdcIncludes="requestId,sessionId,loginId,userId,ipAddress,corpAcctNumber"/>
diff --git a/mvnw b/mvnw
index f5ef4d1..41c0f0c 100755
--- a/mvnw
+++ b/mvnw
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/bin/sh
 # ----------------------------------------------------------------------------
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
@@ -19,7 +19,7 @@
 # ----------------------------------------------------------------------------
 
 # ----------------------------------------------------------------------------
-# Maven2 Start Up Batch script
+# Maven Start Up Batch script
 #
 # Required ENV vars:
 # ------------------
@@ -54,38 +54,16 @@
   CYGWIN*) cygwin=true ;;
   MINGW*) mingw=true;;
   Darwin*) darwin=true
-           #
-           # Look for the Apple JDKs first to preserve the existing behaviour, and then look
-           # for the new JDKs provided by Oracle.
-           #
-           if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then
-             #
-             # Apple JDKs
-             #
-             export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home
-           fi
-
-           if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then
-             #
-             # Apple JDKs
-             #
-             export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
-           fi
-
-           if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then
-             #
-             # Oracle JDKs
-             #
-             export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home
-           fi
-
-           if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then
-             #
-             # Apple JDKs
-             #
-             export JAVA_HOME=`/usr/libexec/java_home`
-           fi
-           ;;
+    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+    if [ -z "$JAVA_HOME" ]; then
+      if [ -x "/usr/libexec/java_home" ]; then
+        export JAVA_HOME="`/usr/libexec/java_home`"
+      else
+        export JAVA_HOME="/Library/Java/Home"
+      fi
+    fi
+    ;;
 esac
 
 if [ -z "$JAVA_HOME" ] ; then
@@ -130,13 +108,12 @@
     CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
 fi
 
-# For Migwn, ensure paths are in UNIX format before anything is touched
+# For Mingw, ensure paths are in UNIX format before anything is touched
 if $mingw ; then
   [ -n "$M2_HOME" ] &&
     M2_HOME="`(cd "$M2_HOME"; pwd)`"
   [ -n "$JAVA_HOME" ] &&
     JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
-  # TODO classpath?
 fi
 
 if [ -z "$JAVA_HOME" ]; then
@@ -187,14 +164,25 @@
 # traverses directory structure from process work directory to filesystem root
 # first directory with .mvn subdirectory is considered project base directory
 find_maven_basedir() {
-  local basedir=$(pwd)
-  local wdir=$(pwd)
+
+  if [ -z "$1" ]
+  then
+    echo "Path not specified to find_maven_basedir"
+    return 1
+  fi
+
+  basedir="$1"
+  wdir="$1"
   while [ "$wdir" != '/' ] ; do
     if [ -d "$wdir"/.mvn ] ; then
       basedir=$wdir
       break
     fi
-    wdir=$(cd "$wdir/.."; pwd)
+    # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+    if [ -d "${wdir}" ]; then
+      wdir=`cd "$wdir/.."; pwd`
+    fi
+    # end of workaround
   done
   echo "${basedir}"
 }
@@ -206,7 +194,94 @@
   fi
 }
 
-export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)}
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+  exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Found .mvn/wrapper/maven-wrapper.jar"
+    fi
+else
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+    fi
+    if [ -n "$MVNW_REPOURL" ]; then
+      jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    else
+      jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    fi
+    while IFS="=" read key value; do
+      case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+      esac
+    done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Downloading from: $jarUrl"
+    fi
+    wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+    if $cygwin; then
+      wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+    fi
+
+    if command -v wget > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found wget ... using wget"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            wget "$jarUrl" -O "$wrapperJarPath"
+        else
+            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+        fi
+    elif command -v curl > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found curl ... using curl"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            curl -o "$wrapperJarPath" "$jarUrl" -f
+        else
+            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+        fi
+
+    else
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Falling back to using Java to download"
+        fi
+        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+        # For Cygwin, switch paths to Windows format before running javac
+        if $cygwin; then
+          javaClass=`cygpath --path --windows "$javaClass"`
+        fi
+        if [ -e "$javaClass" ]; then
+            if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Compiling MavenWrapperDownloader.java ..."
+                fi
+                # Compiling the Java class
+                ("$JAVA_HOME/bin/javac" "$javaClass")
+            fi
+            if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                # Running the downloader
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Running MavenWrapperDownloader.java ..."
+                fi
+                ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+            fi
+        fi
+    fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+  echo $MAVEN_PROJECTBASEDIR
+fi
 MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
 
 # For Cygwin, switch paths to Windows format before running java
@@ -228,7 +303,6 @@
 
 WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
 
-# avoid using MAVEN_CMD_LINE_ARGS below since that would loose parameter escaping in $@
 exec "$JAVACMD" \
   $MAVEN_OPTS \
   -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
diff --git a/mvnw.cmd b/mvnw.cmd
index dcb298e..8611571 100644
--- a/mvnw.cmd
+++ b/mvnw.cmd
@@ -18,7 +18,7 @@
 @REM ----------------------------------------------------------------------------
 
 @REM ----------------------------------------------------------------------------
-@REM Maven2 Start Up Batch script
+@REM Maven Start Up Batch script
 @REM
 @REM Required ENV vars:
 @REM JAVA_HOME - location of a JDK home dir
@@ -26,7 +26,7 @@
 @REM Optional ENV vars
 @REM M2_HOME - location of maven2's installed home dir
 @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
-@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
 @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
 @REM     e.g. to debug Maven itself, use
 @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@@ -35,7 +35,9 @@
 
 @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
 @echo off
-@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
 @if "%MAVEN_BATCH_ECHO%" == "on"  echo %MAVEN_BATCH_ECHO%
 
 @REM set %HOME% to equivalent of $HOME
@@ -80,8 +82,6 @@
 
 :init
 
-set MAVEN_CMD_LINE_ARGS=%MAVEN_CONFIG% %*
-
 @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
 @REM Fallback to current working directory if not found.
 
@@ -117,12 +117,48 @@
 :endReadAdditionalConfig
 
 SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
-
-set WRAPPER_JAR=""%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar""
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
 set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
 
-@REM avoid using MAVEN_CMD_LINE_ARGS below since that would lose parameter escaping in %*
-"%MAVEN_JAVA_EXE%" %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath "%WRAPPER_JAR%" "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+    IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Found %WRAPPER_JAR%
+    )
+) else (
+    if not "%MVNW_REPOURL%" == "" (
+        SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    )
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Couldn't find %WRAPPER_JAR%, downloading it ...
+        echo Downloading from: %DOWNLOAD_URL%
+    )
+
+    powershell -Command "&{"^
+		"$webclient = new-object System.Net.WebClient;"^
+		"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+		"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+		"}"^
+		"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+		"}"
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Finished downloading %WRAPPER_JAR%
+    )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
 if ERRORLEVEL 1 goto error
 goto end
 
diff --git a/pom.xml b/pom.xml
index c31a74d..04239367 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,1746 +1,1768 @@
-<?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/maven-v4_0_0.xsd">

-  <modelVersion>4.0.0</modelVersion>

-  <groupId>org.apache.logging.log4j</groupId>

-  <artifactId>log4j</artifactId>

-  <packaging>pom</packaging>

-  <name>Apache Log4j 2</name>

-  <version>3.0.0-SNAPSHOT</version>

-  <parent>

-    <groupId>org.apache.logging</groupId>

-    <artifactId>logging-parent</artifactId>

-    <version>1</version>

-  </parent>

-  <description>Apache Log4j 2</description>

-  <url>https://logging.apache.org/log4j/2.x/</url>

-  <issueManagement>

-    <system>JIRA</system>

-    <url>https://issues.apache.org/jira/browse/LOG4J2</url>

-  </issueManagement>

-  <ciManagement>

-    <system>Jenkins</system>

-    <url>https://builds.apache.org/job/Log4j%202.x/</url>

-  </ciManagement>

-  <inceptionYear>1999</inceptionYear>

-  <developers>

-    <developer>

-      <id>rgoers</id>

-      <name>Ralph Goers</name>

-      <email>rgoers@apache.org</email>

-      <organization>Nextiva</organization>

-      <roles>

-        <role>PMC Member</role>

-      </roles>

-      <timezone>America/Phoenix</timezone>

-    </developer>

-    <developer>

-      <id>ggregory</id>

-      <name>Gary Gregory</name>

-      <email>ggregory@apache.org</email>

-      <organization>Rocket Software</organization>

-      <roles>

-        <role>PMC Member</role>

-      </roles>

-      <timezone>America/Denver</timezone>

-    </developer>

-    <developer>

-      <id>sdeboy</id>

-      <name>Scott Deboy</name>

-      <email>sdeboy@apache.org</email>

-      <roles>

-        <role>PMC Member</role>

-      </roles>

-      <timezone>America/Los_Angeles</timezone>

-    </developer>

-    <developer>

-      <id>rpopma</id>

-      <name>Remko Popma</name>

-      <email>rpopma@apache.org</email>

-      <roles>

-        <role>PMC Member</role>

-      </roles>

-      <timezone>Asia/Tokyo</timezone>

-      <properties>

-        <picUrl>http://people.apache.org/~rpopma/img/profilepic.jpg</picUrl>

-      </properties>

-    </developer>

-    <developer>

-      <id>nickwilliams</id>

-      <name>Nick Williams</name>

-      <email>nickwilliams@apache.org</email>

-      <roles>

-        <role>PMC Member</role>

-      </roles>

-      <timezone>America/Chicago</timezone>

-    </developer>

-    <developer>

-      <id>mattsicker</id>

-      <name>Matt Sicker</name>

-      <email>mattsicker@apache.org</email>

-      <organization>CloudBees</organization>

-      <roles>

-        <role>PMC Chair</role>

-      </roles>

-      <timezone>America/Chicago</timezone>

-    </developer>

-    <developer>

-      <id>bbrouwer</id>

-      <name>Bruce Brouwer</name>

-      <email>bruce.brouwer@gmail.com</email>

-      <roles>

-        <role>Committer</role>

-      </roles>

-      <timezone>America/Detroit</timezone>

-    </developer>

-    <developer>

-      <id>mikes</id>

-      <name>Mikael Ståldal</name>

-      <email>mikes@apache.org</email>

-      <organization>Spotify</organization>

-      <roles>

-        <role>PMC Member</role>

-      </roles>

-      <timezone>Europe/Stockholm</timezone>

-    </developer>

-    <developer>

-      <id>ckozak</id>

-      <name>Carter Kozak</name>

-      <email>ckozak@apache.org</email>

-      <roles>

-        <role>PMC Member</role>

-      </roles>

-      <timezone>America/New York</timezone>

-    </developer>

-  </developers>

-  <!-- Contributors -->

-  <contributors>

-      <contributor>

-        <name>Murad Ersoy</name>

-        <email>muradersoy@gmail.com</email>

-        <url>https://www.behance.net/muradersoy</url>

-        <roles>

-          <role>Illustrator and Designer</role>

-          <role>created the new Log4j 2 logo.</role>

-        </roles>

-        <timezone>Europe/Istanbul</timezone>

-        <properties>

-          <picUrl>https://mir-s3-cdn-cf.behance.net/user/138/403dcf1521581.54d67f8fb01f7.jpg</picUrl>

-        </properties>

-      </contributor>

-   </contributors>

-  <mailingLists>

-    <mailingList>

-      <name>log4j-user</name>

-      <subscribe>log4j-user-subscribe@logging.apache.org</subscribe>

-      <unsubscribe>log4j-user-unsubscribe@logging.apache.org</unsubscribe>

-      <post>log4j-user@logging.apache.org</post>

-      <archive>https://lists.apache.org/list.html?log4j-user@logging.apache.org</archive>

-      <otherArchives>

-        <otherArchive>http://mail-archives.apache.org/mod_mbox/logging-log4j-user/</otherArchive>

-        <otherArchive>http://marc.info/?l=log4j-user</otherArchive>

-        <otherArchive>http://dir.gmane.org/gmane.comp.jakarta.log4j.user</otherArchive>

-      </otherArchives>

-    </mailingList>

-    <mailingList>

-      <name>dev</name>

-      <subscribe>dev-subscribe@logging.apache.org</subscribe>

-      <unsubscribe>dev-unsubscribe@logging.apache.org</unsubscribe>

-      <post>dev@logging.apache.org</post>

-      <archive>https://lists.apache.org/list.html?dev@logging.apache.org</archive>

-      <otherArchives>

-        <otherArchive>http://mail-archives.apache.org/mod_mbox/logging-dev/</otherArchive>

-        <otherArchive>http://marc.info/?l=dev</otherArchive>

-        <otherArchive>http://dir.gmane.org/gmane.comp.jakarta.log4j.devel</otherArchive>

-      </otherArchives>

-    </mailingList>

-  </mailingLists>

-  <scm>

-    <connection>scm:git:https://gitbox.apache.org/repos/asf/logging-log4j2.git</connection>

-    <developerConnection>scm:git:https://gitbox.apache.org/repos/asf/logging-log4j2.git</developerConnection>

-    <url>https://gitbox.apache.org/repos/asf?p=logging-log4j2.git</url>

-    <tag>log4j-${Log4jReleaseVersion}</tag>

-  </scm>

-  <properties>

-    <!-- make sure to update these for each release! -->

-    <log4jParentDir>${basedir}</log4jParentDir>

-    <Log4jReleaseVersion>2.10.0</Log4jReleaseVersion>

-    <Log4jReleaseManager>Ralph Goers</Log4jReleaseManager>

-    <Log4jReleaseKey>B3D8E1BA</Log4jReleaseKey>

-    <!--<Log4jReleaseManager>Matt Sicker</Log4jReleaseManager> -->

-    <!--<Log4jReleaseKey>748F15B2CF9BA8F024155E6ED7C92B70FA1C814D</Log4jReleaseKey> -->

-    <!-- note that any properties you want available in velocity templates must not use periods! -->

-    <slf4jVersion>1.7.25</slf4jVersion>

-    <logbackVersion>1.2.3</logbackVersion>

-    <jackson1Version>1.9.13</jackson1Version>

-    <jackson2Version>2.10.1</jackson2Version>

-    <springVersion>3.2.18.RELEASE</springVersion>

-    <kubernetes-client.version>4.6.1</kubernetes-client.version>

-    <flumeVersion>1.9.0</flumeVersion>

-    <disruptorVersion>3.4.2</disruptorVersion>

-    <conversantDisruptorVersion>1.2.15</conversantDisruptorVersion>

-    <mongodb2.version>2.14.3</mongodb2.version>

-    <mongodb3.version>3.10.2</mongodb3.version>

-    <groovy.version>2.5.6</groovy.version>

-    <compiler.plugin.version>3.8.1</compiler.plugin.version>

-    <pmd.plugin.version>3.10.0</pmd.plugin.version>

-    <spotbugs.plugin.version>3.1.5</spotbugs.plugin.version>

-    <spotbugs.version>3.1.7</spotbugs.version>

-    <changes.plugin.version>2.12.1</changes.plugin.version>

-    <javadoc.plugin.version>3.2.0</javadoc.plugin.version>

-    <!-- surefire.plugin.version 2.18 yields http://jira.codehaus.org/browse/SUREFIRE-1121, which is fixed in 2.18.1 -->

-    <!-- surefire.plugin.version 2.19 yields https://issues.apache.org/jira/browse/SUREFIRE-1193. -->

-    <!-- all versions after 2.13 yield https://issues.apache.org/jira/browse/SUREFIRE-720 -->

-    <surefire.plugin.version>2.21.0</surefire.plugin.version>

-    <failsafe.plugin.version>2.21.0</failsafe.plugin.version>

-    <checkstyle.plugin.version>3.0.0</checkstyle.plugin.version>

-    <deploy.plugin.version>2.8.2</deploy.plugin.version>

-    <rat.plugin.version>0.13</rat.plugin.version>

-    <pdf.plugin.version>1.2</pdf.plugin.version>

-    <cobertura.plugin.version>2.7</cobertura.plugin.version>

-    <jacoco.plugin.version>0.8.5</jacoco.plugin.version>

-    <release.plugin.version>2.5.3</release.plugin.version>

-    <scm.plugin.version>1.9.5</scm.plugin.version>

-    <jxr.plugin.version>2.5</jxr.plugin.version>

-    <revapi.plugin.version>0.10.5</revapi.plugin.version>

-    <revapi.skip>true</revapi.skip>

-    <site.plugin.version>3.8.2</site.plugin.version>

-    <!-- Maven site depends on Velocity and the escaping rules are different in newer versions. -->

-    <!-- See https://maven.apache.org/plugins/maven-site-plugin/migrate.html -->

-    <velocity.plugin.version>1.5</velocity.plugin.version>

-    <asciidoc.plugin.version>1.5.6</asciidoc.plugin.version>

-    <errorprone.version>2.3.2</errorprone.version>

-    <plexus.errorprone.version>2.8.5</plexus.errorprone.version>

-    <remote.resources.plugin.version>1.5</remote.resources.plugin.version>

-    <manifestfile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestfile>

-    <maven.compiler.source>1.8</maven.compiler.source>

-    <maven.compiler.target>1.8</maven.compiler.target>

-    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

-    <docLabel>Site Documentation</docLabel>

-    <projectDir />

-    <commonsLoggingVersion>1.2</commonsLoggingVersion>

-    <javax.persistence>2.2.1</javax.persistence>

-    <osgi.api.version>6.0.0</osgi.api.version>

-    <activemq.version>5.15.9</activemq.version>

-    <!-- Allow Clirr severity to be overriden by the command-line option -DminSeverity=level -->

-    <minSeverity>info</minSeverity>

-    <jctoolsVersion>1.2.1</jctoolsVersion>

-    <mockitoVersion>2.25.1</mockitoVersion>

-    <argLine>-Xms256m -Xmx1024m</argLine>

-    <javaTargetVersion>1.8</javaTargetVersion>

-    <module.name />

-  </properties>

-  <pluginRepositories>

-    <pluginRepository>

-      <id>apache</id>

-      <url>https://repository.apache.org/content/repositories/releases/</url>

-    </pluginRepository>

-<!--     <pluginRepository> -->

-<!--       <id>apache.snapshots</id> -->

-<!--       <name>Apache snapshots repository</name> -->

-<!--       <url>http://repository.apache.org/content/groups/snapshots</url> -->

-<!--       <snapshots> -->

-<!--         <enabled>true</enabled> -->

-<!--       </snapshots> -->

-<!--     </pluginRepository>     -->

-  </pluginRepositories>

-  <dependencyManagement>

-    <dependencies>

-      <dependency>

-        <groupId>org.slf4j</groupId>

-        <artifactId>slf4j-api</artifactId>

-        <version>${slf4jVersion}</version>

-      </dependency>

-       <dependency>

-        <groupId>org.slf4j</groupId>

-        <artifactId>slf4j-ext</artifactId>

-        <version>${slf4jVersion}</version>

-      </dependency>

-      <dependency>

-        <groupId>ch.qos.logback</groupId>

-        <artifactId>logback-core</artifactId>

-        <version>${logbackVersion}</version>

-        <scope>test</scope>

-      </dependency>

-      <dependency>

-        <groupId>ch.qos.logback</groupId>

-        <artifactId>logback-core</artifactId>

-        <type>test-jar</type>

-        <version>${logbackVersion}</version>

-        <scope>test</scope>

-      </dependency>

-      <dependency>

-        <groupId>org.eclipse.tycho</groupId>

-        <artifactId>org.eclipse.osgi</artifactId>

-        <version>3.12.1.v20170821-1548</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.felix</groupId>

-        <artifactId>org.apache.felix.framework</artifactId>

-        <version>5.6.10</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.maven</groupId>

-        <artifactId>maven-core</artifactId>

-        <version>3.6.0</version>

-      </dependency>

-      <dependency>

-        <groupId>commons-codec</groupId>

-        <artifactId>commons-codec</artifactId>

-        <version>1.12</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.commons</groupId>

-        <artifactId>commons-lang3</artifactId>

-        <version>3.9</version>

-      </dependency>

-      <dependency>

-        <groupId>ch.qos.logback</groupId>

-        <artifactId>logback-classic</artifactId>

-        <version>${logbackVersion}</version>

-        <scope>test</scope>

-      </dependency>

-      <dependency>

-        <groupId>ch.qos.logback</groupId>

-        <artifactId>logback-classic</artifactId>

-        <version>${logbackVersion}</version>

-        <type>test-jar</type>

-        <scope>test</scope>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-api-java9</artifactId>

-        <version>${project.version}</version>

-        <type>zip</type>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-api</artifactId>

-        <version>${project.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-plugins-java9</artifactId>

-        <version>${project.version}</version>

-        <type>zip</type>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-plugins</artifactId>

-        <version>${project.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-1.2-api</artifactId>

-        <version>${project.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-plugins</artifactId>

-        <version>${project.version}</version>

-        <type>test-jar</type>

-        <scope>test</scope>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-api</artifactId>

-        <version>${project.version}</version>

-        <type>test-jar</type>

-        <scope>test</scope>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-core-java9</artifactId>

-        <version>${project.version}</version>

-        <type>zip</type>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-core</artifactId>

-        <version>${project.version}</version>

-      </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.apache.logging.log4j</groupId>

-        <artifactId>log4j-slf4j-impl</artifactId>

-        <version>${project.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-slf4j-impl</artifactId>

-        <version>${project.version}</version>

-        <type>zip</type>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-slf4j18-impl</artifactId>

-        <version>${project.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-jcl</artifactId>

-        <version>${project.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>commons-logging</groupId>

-        <artifactId>commons-logging</artifactId>

-        <version>${commonsLoggingVersion}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-flume-ng</artifactId>

-        <version>${project.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-iostreams</artifactId>

-        <version>${project.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-jul</artifactId>

-        <version>${project.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-jpl</artifactId>

-        <version>${project.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-taglib</artifactId>

-        <version>${project.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.logging.log4j</groupId>

-        <artifactId>log4j-web</artifactId>

-        <version>${project.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>com.sleepycat</groupId>

-        <artifactId>je</artifactId>

-        <version>5.0.73</version>

-      </dependency>

-      <dependency>

-        <groupId>org.osgi</groupId>

-        <artifactId>org.osgi.core</artifactId>

-        <version>${osgi.api.version}</version>

-        <scope>provided</scope>

-      </dependency>

-      <dependency>

-        <groupId>org.fusesource.jansi</groupId>

-        <artifactId>jansi</artifactId>

-        <version>1.17.1</version>

-        <optional>true</optional>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.flume</groupId>

-        <artifactId>flume-ng-sdk</artifactId>

-        <version>${flumeVersion}</version>

-        <exclusions>

-          <exclusion>

-            <groupId>org.codehaus.jackson</groupId>

-            <artifactId>jackson-core-asl</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>org.codehaus.jackson</groupId>

-            <artifactId>jackson-mapper-asl</artifactId>

-          </exclusion>

-        </exclusions>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.flume</groupId>

-        <artifactId>flume-ng-core</artifactId>

-        <version>${flumeVersion}</version>

-        <exclusions>

-          <exclusion>

-            <groupId>org.slf4j</groupId>

-            <artifactId>slf4j-log4j12</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>log4j</groupId>

-            <artifactId>log4j</artifactId>

-          </exclusion>

-        </exclusions>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.flume</groupId>

-        <artifactId>flume-ng-embedded-agent</artifactId>

-        <version>${flumeVersion}</version>

-        <exclusions>

-          <exclusion>

-            <groupId>org.slf4j</groupId>

-            <artifactId>slf4j-log4j12</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>log4j</groupId>

-            <artifactId>log4j</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>org.codehaus.jackson</groupId>

-            <artifactId>jackson-core-asl</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>org.codehaus.jackson</groupId>

-            <artifactId>jackson-mapper-asl</artifactId>

-          </exclusion>

-        </exclusions>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.flume</groupId>

-        <artifactId>flume-ng-node</artifactId>

-        <version>${flumeVersion}</version>

-        <exclusions>

-          <exclusion>

-            <groupId>org.slf4j</groupId>

-            <artifactId>slf4j-log4j12</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>log4j</groupId>

-            <artifactId>log4j</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>org.codehaus.jackson</groupId>

-            <artifactId>jackson-core-asl</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>org.codehaus.jackson</groupId>

-            <artifactId>jackson-mapper-asl</artifactId>

-          </exclusion>

-        </exclusions>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.flume.flume-ng-channels</groupId>

-        <artifactId>flume-file-channel</artifactId>

-        <version>${flumeVersion}</version>

-        <exclusions>

-          <exclusion>

-            <groupId>org.slf4j</groupId>

-            <artifactId>slf4j-log4j12</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>log4j</groupId>

-            <artifactId>log4j</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>org.mortbay.jetty</groupId>

-            <artifactId>servlet-api</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>org.mortbay.jetty</groupId>

-            <artifactId>servlet-api-2.5</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>junit</groupId>

-            <artifactId>junit</artifactId>

-          </exclusion>

-        </exclusions>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.hadoop</groupId>

-        <artifactId>hadoop-core</artifactId>

-        <version>1.2.1</version>

-        <exclusions>

-          <exclusion>

-            <groupId>org.codehaus.jackson</groupId>

-            <artifactId>jackson-core-asl</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>org.codehaus.jackson</groupId>

-            <artifactId>jackson-mapper-asl</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>org.mortbay.jetty</groupId>

-            <artifactId>servlet-api</artifactId>

-          </exclusion>

-          <exclusion>

-            <groupId>junit</groupId>

-            <artifactId>junit</artifactId>

-          </exclusion>

-        </exclusions>

-      </dependency>

-      <!-- Jackson 1 start -->

-      <dependency>

-        <groupId>org.codehaus.jackson</groupId>

-        <artifactId>jackson-core-asl</artifactId>

-        <version>${jackson1Version}</version>

-        <scope>runtime</scope>

-      </dependency>

-      <dependency>

-        <groupId>org.codehaus.jackson</groupId>

-        <artifactId>jackson-mapper-asl</artifactId>

-        <version>${jackson1Version}</version>

-        <scope>runtime</scope>

-      </dependency>

-      <!-- Jackson 1 end -->

-      <!-- Jackson 2 start -->

-      <dependency>

-        <groupId>com.fasterxml.jackson.core</groupId>

-        <artifactId>jackson-core</artifactId>

-        <version>${jackson2Version}</version>

-        <optional>true</optional>

-      </dependency>

-      <dependency>

-        <groupId>com.fasterxml.jackson.core</groupId>

-        <artifactId>jackson-databind</artifactId>

-        <version>${jackson2Version}</version>

-        <optional>true</optional>

-      </dependency>

-      <dependency>

-        <groupId>com.fasterxml.jackson.core</groupId>

-        <artifactId>jackson-annotations</artifactId>

-        <version>${jackson2Version}</version>

-        <optional>true</optional>

-      </dependency>

-      <dependency>

-        <groupId>com.fasterxml.jackson.dataformat</groupId>

-        <artifactId>jackson-dataformat-yaml</artifactId>

-        <version>${jackson2Version}</version>

-        <optional>true</optional>

-      </dependency>

-      <dependency>

-        <groupId>com.fasterxml.jackson.dataformat</groupId>

-        <artifactId>jackson-dataformat-xml</artifactId>

-        <version>${jackson2Version}</version>

-        <optional>true</optional>

-      </dependency>

-      <dependency>

-        <groupId>com.fasterxml.jackson.module</groupId>

-        <artifactId>jackson-module-jaxb-annotations</artifactId>

-        <version>${jackson2Version}</version>

-        <optional>true</optional>

-      </dependency>

-      <!-- Jackson 2 end -->

-      <dependency>

-        <groupId>com.sun.mail</groupId>

-        <artifactId>javax.mail</artifactId>

-        <version>1.6.2</version>

-      </dependency>

-      <dependency>

-        <groupId>org.jboss.spec.javax.jms</groupId>

-        <artifactId>jboss-jms-api_1.1_spec</artifactId>

-        <version>1.0.1.Final</version>

-        <scope>provided</scope>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.activemq</groupId>

-        <artifactId>activemq-broker</artifactId>

-        <version>${activemq.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache.kafka</groupId>

-        <artifactId>kafka-clients</artifactId>

-        <version>1.1.1</version>

-      </dependency>

-      <dependency>

-        <groupId>org.zeromq</groupId>

-        <artifactId>jeromq</artifactId>

-        <version>0.4.3</version>

-      </dependency>

-      <dependency>

-        <groupId>javax.servlet</groupId>

-        <artifactId>servlet-api</artifactId>

-        <version>2.5</version>

-        <scope>provided</scope>

-      </dependency>

-      <dependency>

-        <groupId>com.lmax</groupId>

-        <artifactId>disruptor</artifactId>

-        <version>${disruptorVersion}</version>

-      </dependency>

-      <dependency>

-        <groupId>com.conversantmedia</groupId>

-        <artifactId>disruptor</artifactId>

-        <version>${conversantDisruptorVersion}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.jctools</groupId>

-        <artifactId>jctools-core</artifactId>

-        <version>${jctoolsVersion}</version>

-      </dependency>

-      <dependency>

-        <groupId>junit</groupId>

-        <artifactId>junit</artifactId>

-        <version>4.12</version>

-        <scope>test</scope>

-      </dependency>

-      <dependency>

-        <groupId>org.hamcrest</groupId>

-        <artifactId>hamcrest-all</artifactId>

-        <version>1.3</version>

-        <scope>test</scope>

-      </dependency>

-      <dependency>

-        <groupId>org.codehaus.plexus</groupId>

-        <artifactId>plexus-utils</artifactId>

-        <version>3.2.0</version>

-        <scope>test</scope>

-      </dependency>

-      <dependency>

-        <groupId>org.mockito</groupId>

-        <artifactId>mockito-core</artifactId>

-        <version>${mockitoVersion}</version>

-        <scope>test</scope>

-      </dependency>

-      <dependency>

-        <groupId>org.springframework</groupId>

-        <artifactId>spring-aop</artifactId>

-        <version>${springVersion}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.springframework</groupId>

-        <artifactId>spring-beans</artifactId>

-        <version>${springVersion}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.springframework</groupId>

-        <artifactId>spring-context</artifactId>

-        <version>${springVersion}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.springframework</groupId>

-        <artifactId>spring-core</artifactId>

-        <version>${springVersion}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.springframework</groupId>

-        <artifactId>spring-expression</artifactId>

-        <version>${springVersion}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.springframework</groupId>

-        <artifactId>spring-oxm</artifactId>

-        <version>${springVersion}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.springframework</groupId>

-        <artifactId>spring-test</artifactId>

-        <version>${springVersion}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.springframework</groupId>

-        <artifactId>spring-web</artifactId>

-        <version>${springVersion}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.springframework</groupId>

-        <artifactId>spring-webmvc</artifactId>

-        <version>${springVersion}</version>

-      </dependency>

-      <dependency>

-        <groupId>io.fabric8</groupId>

-        <artifactId>kubernetes-client</artifactId>

-        <version>${kubernetes-client.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.hsqldb</groupId>

-        <artifactId>hsqldb</artifactId>

-        <version>2.4.1</version>

-      </dependency>

-      <dependency>

-        <groupId>com.h2database</groupId>

-        <artifactId>h2</artifactId>

-        <version>1.4.199</version>

-      </dependency>

-      <dependency>

-        <groupId>org.eclipse.persistence</groupId>

-        <artifactId>org.eclipse.persistence.jpa</artifactId>

-        <version>2.7.4</version>

-      </dependency>

-      <dependency>

-        <groupId>org.eclipse.persistence</groupId>

-        <artifactId>javax.persistence</artifactId>

-        <version>${javax.persistence}</version>

-        <scope>provided</scope>

-      </dependency>

-      <dependency>

-        <groupId>org.mongodb</groupId>

-        <artifactId>mongo-java-driver</artifactId>

-        <version>${mongodb2.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.mongodb</groupId>

-        <artifactId>mongodb-driver</artifactId>

-        <version>${mongodb3.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.mongodb</groupId>

-        <artifactId>bson</artifactId>

-        <version>${mongodb3.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.lightcouch</groupId>

-        <artifactId>lightcouch</artifactId>

-        <version>0.0.6</version>

-      </dependency>

-      <dependency>

-        <groupId>com.datastax.cassandra</groupId>

-        <artifactId>cassandra-driver-core</artifactId>

-        <version>3.1.4</version>

-      </dependency>

-      <dependency>

-        <groupId>org.liquibase</groupId>

-        <artifactId>liquibase-core</artifactId>

-        <version>3.5.3</version>

-      </dependency>

-      <dependency>

-        <groupId>net.javacrumbs.json-unit</groupId>

-        <artifactId>json-unit</artifactId>

-        <version>1.31.1</version>

-        <scope>test</scope>

-      </dependency>

-      <dependency>

-        <groupId>org.xmlunit</groupId>

-        <artifactId>xmlunit-core</artifactId>

-        <version>2.6.2</version>

-        <scope>test</scope>

-      </dependency>

-      <dependency>

-        <groupId>org.xmlunit</groupId>

-        <artifactId>xmlunit-matchers</artifactId>

-        <version>2.6.2</version>

-        <scope>test</scope>

-      </dependency>

-      <dependency>

-        <groupId>commons-io</groupId>

-        <artifactId>commons-io</artifactId>

-        <version>2.6</version>

-        <scope>test</scope>

-      </dependency>

-      <!-- Used for testing HttpAppender -->

-      <dependency>

-        <groupId>com.github.tomakehurst</groupId>

-        <artifactId>wiremock</artifactId>

-        <scope>test</scope>

-        <version>2.19.0</version>

-      </dependency>

-      <!-- Used for compressing to formats other than zip and gz -->

-      <dependency>

-        <groupId>org.apache.commons</groupId>

-        <artifactId>commons-compress</artifactId>

-        <version>1.18</version>

-      </dependency>

-      <dependency>

-        <groupId>org.tukaani</groupId>

-        <artifactId>xz</artifactId>

-        <version>1.8</version>

-        <scope>test</scope>

-      </dependency>

-      <!-- Used for the CSV layout -->

-      <dependency>

-        <groupId>org.apache.commons</groupId>

-        <artifactId>commons-csv</artifactId>

-        <version>1.6</version>

-      </dependency>

-      <!-- GC-free -->

-      <dependency>

-        <groupId>com.google.code.java-allocation-instrumenter</groupId>

-        <artifactId>java-allocation-instrumenter</artifactId>

-        <version>3.0.1</version>

-      </dependency>

-      <dependency>

-        <groupId>org.hdrhistogram</groupId>

-        <artifactId>HdrHistogram</artifactId>

-        <version>2.1.9</version>

-      </dependency>

-      <dependency>

-        <groupId>org.apache-extras.beanshell</groupId>

-        <artifactId>bsh</artifactId>

-        <version>2.0b6</version>

-      </dependency>

-      <dependency>

-        <groupId>org.codehaus.groovy</groupId>

-        <artifactId>groovy-jsr223</artifactId>

-        <version>${groovy.version}</version>

-      </dependency>

-      <dependency>

-        <groupId>org.codehaus.groovy</groupId>

-        <artifactId>groovy-dateutil</artifactId>

-        <version>${groovy.version}</version>

-      </dependency>

-      <dependency>

-        <!-- Testing MongoDB -->

-        <groupId>de.flapdoodle.embed</groupId>

-        <artifactId>de.flapdoodle.embed.mongo</artifactId>

-        <version>2.2.0</version>

-        <scope>test</scope>

-      </dependency>

-    </dependencies>

-  </dependencyManagement>

-  <build>

-    <pluginManagement>

-      <plugins>

-        <plugin>

-          <groupId>org.apache.felix</groupId>

-          <artifactId>maven-bundle-plugin</artifactId>

-          <version>3.5.0</version>

-          <inherited>true</inherited>

-          <extensions>true</extensions>

-          <executions>

-            <execution>

-              <goals>

-                <goal>manifest</goal>

-              </goals>

-              <phase>process-classes</phase>

-            </execution>

-          </executions>

-        </plugin>

-        <plugin>

-          <groupId>org.apache.maven.plugins</groupId>

-          <artifactId>maven-changes-plugin</artifactId>

-          <version>${changes.plugin.version}</version>

-        </plugin>

-        <plugin>

-          <groupId>org.apache.maven.plugins</groupId>

-          <artifactId>maven-release-plugin</artifactId>

-          <version>${release.plugin.version}</version>

-        </plugin>

-        <plugin>

-          <groupId>org.apache.maven.plugins</groupId>

-          <artifactId>maven-scm-plugin</artifactId>

-          <version>${scm.plugin.version}</version>

-        </plugin>

-        <plugin>

-          <groupId>org.apache.maven.plugins</groupId>

-          <artifactId>maven-checkstyle-plugin</artifactId>

-          <version>${checkstyle.plugin.version}</version>

-        </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>

-            <additionalparam>${javadoc.opts}</additionalparam>

-          </configuration>

-        </plugin>

-        <plugin>

-          <groupId>org.apache.maven.plugins</groupId>

-          <artifactId>maven-pmd-plugin</artifactId>

-          <version>${pmd.plugin.version}</version>

-        </plugin>

-        <!-- some nice default compiler options -->

-        <plugin>

-          <groupId>org.apache.maven.plugins</groupId>

-          <artifactId>maven-compiler-plugin</artifactId>

-          <version>${compiler.plugin.version}</version>

-          <configuration>

-            <source>${maven.compiler.source}</source>

-            <target>${maven.compiler.target}</target>

-            <showDeprecation>true</showDeprecation>

-            <showWarnings>true</showWarnings>

-            <encoding>UTF-8</encoding>

-            <fork>true</fork>

-            <meminitial>256</meminitial>

-            <maxmem>1024</maxmem>

-            <compilerArguments>

-              <Xmaxwarns>10000</Xmaxwarns>

-              <Xlint />

-            </compilerArguments>

-            <compilerId>javac-with-errorprone</compilerId>

-            <forceJavacCompilerUse>true</forceJavacCompilerUse>

-            <parameters>true</parameters>

-          </configuration>

-          <dependencies>

-            <dependency>

-              <groupId>org.codehaus.plexus</groupId>

-              <artifactId>plexus-compiler-javac-errorprone</artifactId>

-              <version>${plexus.errorprone.version}</version>

-            </dependency>

-            <dependency>

-              <groupId>com.google.errorprone</groupId>

-              <artifactId>error_prone_core</artifactId>

-              <version>${errorprone.version}</version>

-            </dependency>

-          </dependencies>

-        </plugin>

-        <plugin>

-          <groupId>org.apache.maven.plugins</groupId>

-          <artifactId>maven-surefire-plugin</artifactId>

-          <version>${surefire.plugin.version}</version>

-        </plugin>

-        <plugin>

-          <groupId>org.apache.maven.plugins</groupId>

-          <artifactId>maven-failsafe-plugin</artifactId>

-          <version>${failsafe.plugin.version}</version>

-        </plugin>

-        <plugin>

-          <groupId>org.apache.maven.plugins</groupId>

-          <artifactId>maven-source-plugin</artifactId>

-          <version>3.0.1</version>

-          <executions>

-            <execution>

-              <id>attach-sources</id>

-              <phase>verify</phase>

-              <goals>

-                <goal>jar-no-fork</goal>

-                <goal>test-jar-no-fork</goal>

-              </goals>

-            </execution>

-          </executions>

-        </plugin>

-        <plugin>

-          <groupId>org.apache.maven.plugins</groupId>

-          <artifactId>maven-jxr-plugin</artifactId>

-          <version>${jxr.plugin.version}</version>

-        </plugin>

-        <plugin>

-          <groupId>org.eluder.coveralls</groupId>

-          <artifactId>coveralls-maven-plugin</artifactId>

-          <version>4.3.0</version>

-        </plugin>

-        <plugin>

-          <groupId>org.jacoco</groupId>

-          <artifactId>jacoco-maven-plugin</artifactId>

-          <version>${jacoco.plugin.version}</version>

-          <executions>

-            <execution>

-                <id>prepare-agent</id>

-                <goals>

-                  <goal>prepare-agent</goal>

-                </goals>

-            </execution>

-            <execution>

-              <id>default-report</id>

-              <phase>prepare-package</phase>

-              <goals>

-                <goal>report</goal>

-              </goals>

-            </execution>

-          </executions>

-        </plugin>

-        <plugin>

-          <groupId>org.apache.maven.plugins</groupId>

-          <artifactId>maven-assembly-plugin</artifactId>

-          <version>3.1.0</version>

-        </plugin>

-        <plugin>

-          <groupId>com.github.spotbugs</groupId>

-          <artifactId>spotbugs-maven-plugin</artifactId>

-          <version>${spotbugs.plugin.version}</version>

-          <dependencies>

-            <!-- overwrite dependency on spotbugs if you want to specify the version of spotbugs -->

-            <dependency>

-              <groupId>com.github.spotbugs</groupId>

-              <artifactId>spotbugs</artifactId>

-              <version>${spotbugs.version}</version>

-            </dependency>

-          </dependencies>

-        </plugin>

-        <plugin>

-          <groupId>org.apache.maven.plugins</groupId>

-          <artifactId>maven-jar-plugin</artifactId>

-          <version>3.1.0</version>

-          <executions>

-            <execution>

-              <id>default-jar</id>

-              <goals>

-                <goal>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>

-                    <Automatic-Module-Name>${module.name}</Automatic-Module-Name>

-                  </manifestEntries>

-                </archive>

-              </configuration>

-            </execution>

-          </executions>

-        </plugin>

-      </plugins>

-    </pluginManagement>

-    <plugins>

-      <plugin>

-        <groupId>org.apache.maven.plugins</groupId>

-        <artifactId>maven-checkstyle-plugin</artifactId>

-      </plugin>

-      <plugin>

-        <artifactId>maven-clean-plugin</artifactId>

-        <version>3.1.0</version>

-      </plugin>

-      <plugin>

-        <artifactId>maven-resources-plugin</artifactId>

-        <version>3.0.2</version>

-        <executions>

-          <execution>

-            <id>copy-sitecss</id>

-            <!-- fetch site.xml before creating site documentation -->

-            <phase>pre-site</phase>

-            <goals>

-              <goal>copy-resources</goal>

-            </goals>

-            <configuration>

-              <outputDirectory>${project.build.directory}/site</outputDirectory>

-              <resources>

-                <resource>

-                  <directory>${log4jParentDir}/src/site/resources</directory>

-                  <includes>

-                    <include>**/*</include>

-                  </includes>

-                </resource>

-              </resources>

-            </configuration>

-          </execution>

-        </executions>

-      </plugin>

-      <plugin>

-        <groupId>org.apache.maven.plugins</groupId>

-        <artifactId>maven-surefire-plugin</artifactId>

-        <version>${surefire.plugin.version}</version>

-        <configuration>

-          <systemPropertyVariables>

-            <java.awt.headless>true</java.awt.headless>

-          </systemPropertyVariables>

-          <forkCount>1</forkCount>

-          <reuseForks>false</reuseForks>

-          <excludes>

-            <exclude>${log4j.skip.test1}</exclude>

-            <exclude>${log4j.skip.test2}</exclude>

-          </excludes>

-        </configuration>

-      </plugin>

-      <plugin>

-        <groupId>org.apache.maven.plugins</groupId>

-        <artifactId>maven-failsafe-plugin</artifactId>

-        <version>${failsafe.plugin.version}</version>

-        <executions>

-          <execution>

-            <goals>

-              <goal>integration-test</goal>

-              <goal>verify</goal>

-            </goals>

-          </execution>

-        </executions>

-        <configuration>

-          <systemPropertyVariables>

-            <java.awt.headless>true</java.awt.headless>

-          </systemPropertyVariables>

-          <argLine>-Xms256m -Xmx1024m</argLine>

-          <forkCount>1</forkCount>

-          <reuseForks>false</reuseForks>

-          <encoding>UTF-8</encoding>

-        </configuration>

-      </plugin>

-      <plugin>

-        <groupId>org.codehaus.mojo</groupId>

-        <artifactId>build-helper-maven-plugin</artifactId>

-        <version>1.12</version>

-        <executions>

-          <execution>

-            <id>timestamp-property</id>

-            <goals>

-              <goal>timestamp-property</goal>

-            </goals>

-            <phase>pre-site</phase>

-            <configuration>

-              <name>currentYear</name>

-              <pattern>yyyy</pattern>

-            </configuration>

-          </execution>

-        </executions>

-      </plugin>

-      <plugin>

-        <groupId>org.apache.maven.plugins</groupId>

-        <artifactId>maven-site-plugin</artifactId>

-        <version>${site.plugin.version}</version>

-        <dependencies>

-          <dependency>

-            <groupId>org.apache.maven.wagon</groupId>

-            <artifactId>wagon-ssh</artifactId>

-            <version>3.1.0</version>

-          </dependency>

-          <dependency>

-            <groupId>org.asciidoctor</groupId>

-            <artifactId>asciidoctor-maven-plugin</artifactId>

-            <version>${asciidoc.plugin.version}</version>

-          </dependency>

-        </dependencies>

-        <configuration>

-          <!-- only build English site even on other language OS -->

-          <locales>en</locales>

-          <!-- Exclude the navigation file for Maven 1 sites

-               and the changes file used by the changes-plugin,

-               as they interfere with the site generation. -->

-          <moduleExcludes>

-            <xdoc>navigation.xml,changes.xml</xdoc>

-          </moduleExcludes>

-          <asciidoc>

-            <attributes>

-              <!-- copy any site properties wanted in asciidoc files -->

-              <Log4jReleaseVersion>${Log4jReleaseVersion}</Log4jReleaseVersion>

-              <Log4jReleaseManager>${Log4jReleaseManager}</Log4jReleaseManager>

-              <Log4jReleaseKey>${Log4jReleaseKey}</Log4jReleaseKey>

-            </attributes>

-          </asciidoc>

-        </configuration>

-      </plugin>

-      <!-- <plugin>

-        <groupId>org.codehaus.mojo</groupId>

-        <artifactId>cobertura-maven-plugin</artifactId>

-        <executions>

-          <execution>

-            <id>clean</id>

-            <goals>

-              <goal>clean</goal>

-            </goals>

-          </execution>

-        </executions>

-      </plugin> -->

-      <!-- We need to disable the standard ASF configuration to be able to publish our own notice and license files -->

-      <plugin>

-        <groupId>org.apache.maven.plugins</groupId>

-        <artifactId>maven-remote-resources-plugin</artifactId>

-        <executions>

-          <execution>

-            <goals>

-              <goal>process</goal>

-            </goals>

-            <configuration>

-              <skip>true</skip>

-              <resourceBundles />

-            </configuration>

-          </execution>

-        </executions>

-      </plugin>

-      <plugin>

-        <groupId>org.apache.maven.plugins</groupId>

-        <artifactId>maven-pdf-plugin</artifactId>

-        <version>${pdf.plugin.version}</version>

-        <executions>

-          <execution>

-            <id>pdf</id>

-            <phase>site</phase>

-            <goals>

-              <goal>pdf</goal>

-            </goals>

-            <configuration>

-              <outputDirectory>${project.reporting.outputDirectory}</outputDirectory>

-            </configuration>

-          </execution>

-        </executions>

-      </plugin>

-      <plugin>

-        <groupId>org.apache.maven.plugins</groupId>

-        <artifactId>maven-source-plugin</artifactId>

-      </plugin>

-      <!-- RAT report -->

-      <plugin>

-        <groupId>org.apache.rat</groupId>

-        <artifactId>apache-rat-plugin</artifactId>

-        <version>${rat.plugin.version}</version>

-        <configuration>

-          <excludes>

-            <!-- Matches other RAT configurations in this POM -->

-            <exclude>src/main/resources/META-INF/services/**/*</exclude>

-            <!-- IntelliJ files -->

-            <exclude>.idea/**/*</exclude>

-            <exclude>src/test/resources/**/*</exclude>

-            <!-- IDE settings imports -->

-            <exclude>src/ide/**</exclude>

-            <!-- does it even make sense to apply a license to a GPG signature? -->

-            <exclude>**/*.asc</exclude>

-            <!-- jQuery is MIT-licensed, but RAT can't figure it out -->

-            <exclude>src/site/resources/js/jquery.js</exclude>

-            <exclude>src/site/resources/js/jquery.min.js</exclude>

-            <!-- highlight.js is BSD3-licensed -->

-            <exclude>src/site/resources/js/highlight.pack.js</exclude>

-            <!-- Generated files -->

-            <exclude>log4j-distribution/target/**/*</exclude>

-            <exclude>log4j-distribution/.project</exclude>

-            <exclude>log4j-distribution/.settings/**</exclude>

-            <exclude>velocity.log</exclude>

-            <!-- Other -->

-            <exclude>felix-cache/**</exclude>

-            <exclude>RELEASE-NOTES.md</exclude>

-            <exclude>**/*.yml</exclude>

-            <exclude>**/*.yaml</exclude>

-            <exclude>**/*.json</exclude>

-            <excllude>**/images/*.drawio</excllude>

-            <exclude>**/fluent-bit.conf</exclude>

-            <exclude>**/rabbitmq.config</exclude>

-            <exclude>**/MANIFEST.MF</exclude>

-          </excludes>

-        </configuration>

-      </plugin>

-      <!-- DOAP (RDF) metadata generation -->

-      <plugin>

-        <groupId>org.apache.maven.plugins</groupId>

-        <artifactId>maven-doap-plugin</artifactId>

-        <version>1.2</version>

-        <configuration>

-          <doapOptions>

-            <programmingLanguage>Java</programmingLanguage>

-            <category>library</category>

-          </doapOptions>

-          <asfExtOptions>

-            <charter>

-              The Apache Logging Services Project creates and maintains open-source software related to the logging of

-              application behavior and released at no charge to the public.

-            </charter>

-            <pmc>https://logging.apache.org</pmc>

-          </asfExtOptions>

-        </configuration>

-        <executions>

-          <execution>

-            <id>site</id>

-            <phase>site</phase>

-            <goals>

-              <goal>generate</goal>

-            </goals>

-          </execution>

-        </executions>

-      </plugin>

-      <plugin>

-        <groupId>org.revapi</groupId>

-        <artifactId>revapi-maven-plugin</artifactId>

-        <version>${revapi.plugin.version}</version>

-        <dependencies>

-          <dependency>

-            <groupId>org.revapi</groupId>

-            <artifactId>revapi-java</artifactId>

-            <version>0.18.2</version>

-          </dependency>

-        </dependencies>

-        <executions>

-          <execution>

-            <goals><goal>check</goal></goals>

-            <configuration>

-              <checkDependencies>false</checkDependencies>

-              <skip>${revapi.skip}</skip>

-              <failOnMissingConfigurationFiles>false</failOnMissingConfigurationFiles>

-              <analysisConfigurationFiles>

-                <path>revapi.json</path>

-              </analysisConfigurationFiles>

-              <analysisConfiguration><![CDATA[

-[

-  {

-     "extension": "revapi.java",

-     "configuration": {

-       "missing-classes": {

-         "behavior": "report",

-         "ignoreMissingAnnotations": false

-       },

-       "reportUsesFor": [

-          "java.missing.newClass",

-          "java.class.nonPublicPartOfAPI"

-       ],

-       "filter": {

-         "classes": {

-           "regex": true,

-           "include": [

-             "org\\.apache\\.logging\\.log4j(\\..+)?"

-           ]

-         },

-         "packages": {

-           "regex": true,

-           "include": [

-             "org\\.apache\\.logging\\.log4j(\\..+)?"

-           ]

-         }

-       }

-     }

-  }

-]

-              ]]></analysisConfiguration>

-            </configuration>

-          </execution>

-        </executions>

-      </plugin>

-    </plugins>

-  </build>

-  <reporting>

-    <plugins>

-      <!-- Changes report -->

-      <plugin>

-        <groupId>org.apache.maven.plugins</groupId>

-        <artifactId>maven-changes-plugin</artifactId>

-        <version>${changes.plugin.version}</version>

-        <reportSets>

-          <reportSet>

-            <reports>

-              <report>changes-report</report>

-              <report>jira-report</report>

-            </reports>

-          </reportSet>

-        </reportSets>

-        <configuration>

-          <statusIds>Resolved, Closed</statusIds>

-          <columnNames>Type,Key,Summary,Assignee,Status,Resolution,Fix Version</columnNames>

-          <useJql>true</useJql>

-        </configuration>

-      </plugin>

-      <plugin>

-        <groupId>org.apache.maven.plugins</groupId>

-        <artifactId>maven-project-info-reports-plugin</artifactId>

-        <version>2.9</version>

-        <reportSets>

-          <reportSet>

-            <reports>

-              <report>index</report>

-              <report>dependencies</report>

-              <report>dependency-info</report>

-              <report>dependency-convergence</report>

-              <report>dependency-management</report>

-              <report>project-team</report>

-              <report>mailing-list</report>

-              <report>issue-tracking</report>

-              <report>license</report>

-              <report>scm</report>

-              <report>summary</report>

-            </reports>

-          </reportSet>

-        </reportSets>

-        <configuration>

-          <!-- you'd think these would be the defaults, right? -->

-          <customBundle>${project.basedir}/src/site/custom/project-info-report.properties</customBundle>

-          <webAccessUrl>${project.scm.url}</webAccessUrl>

-          <anonymousConnection>${project.scm.connection}</anonymousConnection>

-          <developerConnection>${project.scm.developerConnection}</developerConnection>

-          <scmTag>log4j-${Log4jReleaseVersion}</scmTag>

-        </configuration>

-      </plugin>

-      <!-- Surefire report -->

-      <plugin>

-        <groupId>org.apache.maven.plugins</groupId>

-        <artifactId>maven-surefire-report-plugin</artifactId>

-        <version>${surefire.plugin.version}</version>

-        <reportSets>

-          <reportSet>

-            <id>integration-tests</id>

-            <reports>

-              <report>failsafe-report-only</report>

-            </reports>

-          </reportSet>

-        </reportSets>

-      </plugin>

-      <!-- RAT report -->

-      <plugin>

-        <groupId>org.apache.rat</groupId>

-        <artifactId>apache-rat-plugin</artifactId>

-        <version>${rat.plugin.version}</version>

-        <configuration>

-          <excludes>

-            <!-- Matches other RAT configurations in this POM -->

-            <exclude>src/main/resources/META-INF/services/**/*</exclude>

-            <!-- IntelliJ files -->

-            <exclude>.idea/**/*</exclude>

-            <exclude>src/test/resources/**/*</exclude>

-            <!-- IDE settings imports -->

-            <exclude>src/ide/**</exclude>

-            <!-- does it even make sense to apply a license to a GPG signature? -->

-            <exclude>**/*.asc</exclude>

-            <!-- jQuery is MIT-licensed, but RAT can't figure it out -->

-            <exclude>src/site/resources/js/jquery.js</exclude>

-            <exclude>src/site/resources/js/jquery.min.js</exclude>

-            <!-- highlight.js is BSD3-licensed -->

-            <exclude>src/site/resources/js/highlight.pack.js</exclude>

-            <!-- Generated files -->

-            <exclude>log4j-distribution/target/**/*</exclude>

-            <exclude>log4j-distribution/.project</exclude>

-            <exclude>log4j-distribution/.settings/**</exclude>

-            <exclude>velocity.log</exclude>

-            <!-- Other -->

-            <exclude>felix-cache/**</exclude>

-            <exclude>RELEASE-NOTES.txt</exclude>

-            <exclude>**/*.yml</exclude>

-            <exclude>**/*.yaml</exclude>

-            <exclude>**/*.json</exclude>

-            <excllude>**/images/*.drawio</excllude>

-            <exclude>**/fluent-bit.conf</exclude>

-            <exclude>**/rabbitmq.config</exclude>

-            <exclude>**/MANIFEST.MF</exclude>

-          </excludes>

-        </configuration>

-      </plugin>

-      <plugin>

-        <groupId>org.revapi</groupId>

-        <artifactId>revapi-maven-plugin</artifactId>

-        <version>${revapi.plugin.version}</version>

-        <reportSets>

-          <reportSet>

-            <reports>

-              <report>report</report>

-            </reports>

-          </reportSet>

-        </reportSets>

-      </plugin>

-    </plugins>

-  </reporting>

-  <distributionManagement>

-    <downloadUrl>https://logging.apache.org/log4j/2.x/download.html</downloadUrl>

-    <!-- site is only included to make maven-site-plugin stop complaining -->

-    <site>

-      <id>www.example.com</id>

-      <url>scp://www.example.com/www/docs/project/</url>

-    </site>

-  </distributionManagement>

-  <modules>

-    <module>log4j-api-java9</module>

-    <module>log4j-api</module>

-    <module>log4j-plugins-java9</module>

-    <module>log4j-plugins</module>

-    <module>log4j-core-java9</module>

-    <module>log4j-core</module>

-    <module>log4j-layout-jackson</module>

-    <module>log4j-layout-jackson-json</module>

-    <module>log4j-layout-jackson-xml</module>

-    <module>log4j-layout-jackson-yaml</module>

-    <module>log4j-core-its</module>

-    <module>log4j-1.2-api</module>

-    <module>log4j-slf4j-impl</module>

-    <module>log4j-slf4j18-impl</module>

-    <module>log4j-to-slf4j</module>

-    <module>log4j-jcl</module>

-    <module>log4j-csv</module>

-    <module>log4j-flume-ng</module>

-    <module>log4j-taglib</module>

-    <module>log4j-jmx-gui</module>

-    <module>log4j-samples</module>

-    <module>log4j-bom</module>

-    <module>log4j-jdbc</module>

-    <module>log4j-jdbc-dbcp2</module>

-    <module>log4j-jpa</module>

-    <module>log4j-jeromq</module>

-    <module>log4j-jms</module>

-    <module>log4j-kafka</module>

-    <module>log4j-couchdb</module>

-    <module>log4j-mongodb2</module>

-    <module>log4j-mongodb3</module>

-    <module>log4j-cassandra</module>

-    <module>log4j-web</module>

-    <module>log4j-perf</module>

-    <module>log4j-iostreams</module>

-    <module>log4j-jul</module>

-    <module>log4j-jpl</module>

-    <module>log4j-liquibase</module>

-    <module>log4j-appserver</module>

-    <module>log4j-smtp</module>

-    <module>log4j-osgi</module>

-    <module>log4j-docker</module>

-    <module>log4j-kubernetes</module>

-    <module>log4j-spring-cloud-config</module>

-  </modules>

-  <profiles>

-    <profile>

-      <id>pdf</id>

-      <build>

-        <plugins>

-          <plugin>

-            <groupId>org.apache.maven.plugins</groupId>

-            <artifactId>maven-pdf-plugin</artifactId>

-            <version>${pdf.plugin.version}</version>

-            <executions>

-              <execution>

-                <id>pdf</id>

-                <phase>generate-resources</phase>

-                <goals>

-                  <goal>pdf</goal>

-                </goals>

-                <configuration>

-                  <outputDirectory>${project.reporting.outputDirectory}</outputDirectory>

-                </configuration>

-              </execution>

-            </executions>

-          </plugin>

-        </plugins>

-      </build>

-    </profile>

-    <profile>

-      <id>release-notes</id>

-      <build>

-        <plugins>

-          <plugin>

-            <groupId>org.apache.maven.plugins</groupId>

-            <artifactId>maven-changes-plugin</artifactId>

-            <version>${changes.plugin.version}</version>

-            <configuration>

-              <template>announcement.vm</template>

-              <templateDirectory>src/changes</templateDirectory>

-              <runOnlyAtExecutionRoot>true</runOnlyAtExecutionRoot>

-              <announcementDirectory>.</announcementDirectory>

-              <announcementFile>RELEASE-NOTES.md</announcementFile>

-              <issueManagementSystems>

-                <issueManagementSystem>changes.xml</issueManagementSystem>

-                <!--<issueManagementSystem>JIRA</issueManagementSystem> -->

-              </issueManagementSystems>

-              <version>${Log4jReleaseVersion}</version>

-              <announceParameters>

-                <releaseVersion>${Log4jReleaseVersion}</releaseVersion>

-                <releaseCount>${Log4jReleaseCount}</releaseCount>

-              </announceParameters>

-              <useJql>true</useJql>

-            </configuration>

-            <executions>

-              <execution>

-                <id>create-release-notes</id>

-                <phase>generate-resources</phase>

-                <goals>

-                  <goal>announcement-generate</goal>

-                </goals>

-              </execution>

-            </executions>

-          </plugin>

-        </plugins>

-      </build>

-    </profile>

-    <profile>

-      <id>apache-release</id>

-      <build>

-        <plugins>

-          <plugin>

-            <artifactId>maven-assembly-plugin</artifactId>

-            <executions>

-              <execution>

-                <id>source-release-assembly</id>

-                <configuration>

-                  <skipAssembly>true</skipAssembly>

-                </configuration>

-              </execution>

-            </executions>

-          </plugin>

-        </plugins>

-      </build>

-      <modules>

-        <module>log4j-distribution</module>

-      </modules>

-    </profile>

-    <profile>

-      <id>rat</id>

-      <build>

-        <plugins>

-          <!-- RAT report -->

-          <plugin>

-            <groupId>org.apache.rat</groupId>

-            <artifactId>apache-rat-plugin</artifactId>

-            <version>${rat.plugin.version}</version>

-            <configuration>

-              <excludes>

-                <!-- Matches other RAT configurations in this POM -->

-                <exclude>src/main/resources/META-INF/services/**/*</exclude>

-                <!-- IntelliJ files -->

-                <exclude>.idea/**/*</exclude>

-                <exclude>src/test/resources/**/*</exclude>

-                <!-- IDE settings imports -->

-                <exclude>src/ide/**</exclude>

-                <!-- does it even make sense to apply a license to a GPG signature? -->

-                <exclude>**/*.asc</exclude>

-                <!-- jQuery is MIT-licensed, but RAT can't figure it out -->

-                <exclude>src/site/resources/js/jquery.js</exclude>

-                <exclude>src/site/resources/js/jquery.min.js</exclude>

-                <!-- highlight.js is BSD3-licensed -->

-                <exclude>src/site/resources/js/highlight.pack.js</exclude>

-                <!-- Generated files -->

-                <exclude>log4j-distribution/target/**/*</exclude>

-                <exclude>log4j-distribution/.project</exclude>

-                <exclude>log4j-distribution/.settings/**</exclude>

-                <exclude>velocity.log</exclude>

-                <!-- Other -->

-                <exclude>felix-cache/**</exclude>

-                <exclude>RELEASE-NOTES.md</exclude>

-                <exclude>**/*.yml</exclude>

-                <exclude>**/*.yaml</exclude>

-                <exclude>**/*.json</exclude>

-                <excllude>**/images/*.drawio</excllude>

-                <exclude>**/fluent-bit.conf</exclude>

-                <exclude>**/rabbitmq.config</exclude>

-                <exclude>**/MANIFEST.MF</exclude>

-              </excludes>

-            </configuration>

-            <executions>

-              <execution>

-                <phase>verify</phase>

-                <goals>

-                  <goal>check</goal>

-                </goals>

-              </execution>

-            </executions>

-          </plugin>

-        </plugins>

-      </build>

-    </profile>

-    <profile>

-      <!-- http://www.yourkit.com/docs/80/help/agent.jsp -->

-      <id>yourkit-mac</id>

-      <!--

-      <activation>

-        <os>

-          <family>Mac</family>

-        </os>

-        <file>

-          <exists>${yourkit.home}/bin/mac/libyjpagent.jnilib</exists>

-        </file>

-      </activation>

-      -->

-      <properties>

-        <yourkit.home>/Applications/YJP.app</yourkit.home>

-      </properties>

-      <dependencies>

-        <dependency>

-          <groupId>com.yourkit</groupId>

-          <artifactId>yjp-controller-api-redist</artifactId>

-          <version>2013</version>

-          <scope>system</scope>

-          <systemPath>${yourkit.home}/lib/yjp-controller-api-redist.jar</systemPath>

-        </dependency>

-      </dependencies>

-      <build>

-        <plugins>

-          <plugin>

-            <groupId>org.apache.maven.plugins</groupId>

-            <artifactId>maven-surefire-plugin</artifactId>

-            <configuration>

-              <argLine>-agentpath:"${yourkit.home}/bin/mac/libyjpagent.jnilib"</argLine>

-            </configuration>

-          </plugin>

-          <plugin>

-            <artifactId>maven-failsafe-plugin</artifactId>

-            <configuration>

-              <argLine>-agentpath:"${yourkit.home}/bin/mac/libyjpagent.jnilib"</argLine>

-            </configuration>

-          </plugin>

-        </plugins>

-      </build>

-    </profile>

-    <profile>

-      <id>java8-doclint-disabled</id>

-      <activation>

-        <jdk>[1.8,)</jdk>

-      </activation>

-      <properties>

-        <javadoc.opts>-Xdoclint:none</javadoc.opts>

-      </properties>

-    </profile>

-  </profiles>

-</project>

+<?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/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.apache.logging.log4j</groupId>
+  <artifactId>log4j</artifactId>
+  <packaging>pom</packaging>
+  <name>Apache Log4j 2</name>
+  <version>3.0.0-SNAPSHOT</version>
+  <parent>
+    <groupId>org.apache.logging</groupId>
+    <artifactId>logging-parent</artifactId>
+    <version>2</version>
+    <relativePath/>
+  </parent>
+  <description>Apache Log4j 2</description>
+  <url>https://logging.apache.org/log4j/2.x/</url>
+  <issueManagement>
+    <system>JIRA</system>
+    <url>https://issues.apache.org/jira/browse/LOG4J2</url>
+  </issueManagement>
+  <ciManagement>
+    <system>Jenkins</system>
+    <url>https://builds.apache.org/job/Log4j%202.x/</url>
+  </ciManagement>
+  <inceptionYear>1999</inceptionYear>
+  <developers>
+    <developer>
+      <id>rgoers</id>
+      <name>Ralph Goers</name>
+      <email>rgoers@apache.org</email>
+      <organization>Nextiva</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+      <timezone>America/Phoenix</timezone>
+    </developer>
+    <developer>
+      <id>ggregory</id>
+      <name>Gary Gregory</name>
+      <email>ggregory@apache.org</email>
+      <organization>Rocket Software</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+      <timezone>America/Denver</timezone>
+    </developer>
+    <developer>
+      <id>sdeboy</id>
+      <name>Scott Deboy</name>
+      <email>sdeboy@apache.org</email>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+      <timezone>America/Los_Angeles</timezone>
+    </developer>
+    <developer>
+      <id>rpopma</id>
+      <name>Remko Popma</name>
+      <email>rpopma@apache.org</email>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+      <timezone>Asia/Tokyo</timezone>
+      <properties>
+        <picUrl>http://people.apache.org/~rpopma/img/profilepic.jpg</picUrl>
+      </properties>
+    </developer>
+    <developer>
+      <id>nickwilliams</id>
+      <name>Nick Williams</name>
+      <email>nickwilliams@apache.org</email>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+      <timezone>America/Chicago</timezone>
+    </developer>
+    <developer>
+      <id>mattsicker</id>
+      <name>Matt Sicker</name>
+      <email>mattsicker@apache.org</email>
+      <organization>CloudBees</organization>
+      <roles>
+        <role>PMC Chair</role>
+      </roles>
+      <timezone>America/Chicago</timezone>
+    </developer>
+    <developer>
+      <id>bbrouwer</id>
+      <name>Bruce Brouwer</name>
+      <email>bruce.brouwer@gmail.com</email>
+      <roles>
+        <role>Committer</role>
+      </roles>
+      <timezone>America/Detroit</timezone>
+    </developer>
+    <developer>
+      <id>mikes</id>
+      <name>Mikael Ståldal</name>
+      <email>mikes@apache.org</email>
+      <organization>Spotify</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+      <timezone>Europe/Stockholm</timezone>
+    </developer>
+    <developer>
+      <id>ckozak</id>
+      <name>Carter Kozak</name>
+      <email>ckozak@apache.org</email>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+      <timezone>America/New York</timezone>
+    </developer>
+  </developers>
+  <!-- Contributors -->
+  <contributors>
+      <contributor>
+        <name>Murad Ersoy</name>
+        <email>muradersoy@gmail.com</email>
+        <url>https://www.behance.net/muradersoy</url>
+        <roles>
+          <role>Illustrator and Designer</role>
+          <role>created the new Log4j 2 logo.</role>
+        </roles>
+        <timezone>Europe/Istanbul</timezone>
+        <properties>
+          <picUrl>https://mir-s3-cdn-cf.behance.net/user/138/403dcf1521581.54d67f8fb01f7.jpg</picUrl>
+        </properties>
+      </contributor>
+   </contributors>
+  <mailingLists>
+    <mailingList>
+      <name>log4j-user</name>
+      <subscribe>log4j-user-subscribe@logging.apache.org</subscribe>
+      <unsubscribe>log4j-user-unsubscribe@logging.apache.org</unsubscribe>
+      <post>log4j-user@logging.apache.org</post>
+      <archive>https://lists.apache.org/list.html?log4j-user@logging.apache.org</archive>
+      <otherArchives>
+        <otherArchive>http://mail-archives.apache.org/mod_mbox/logging-log4j-user/</otherArchive>
+        <otherArchive>http://marc.info/?l=log4j-user</otherArchive>
+        <otherArchive>http://dir.gmane.org/gmane.comp.jakarta.log4j.user</otherArchive>
+      </otherArchives>
+    </mailingList>
+    <mailingList>
+      <name>dev</name>
+      <subscribe>dev-subscribe@logging.apache.org</subscribe>
+      <unsubscribe>dev-unsubscribe@logging.apache.org</unsubscribe>
+      <post>dev@logging.apache.org</post>
+      <archive>https://lists.apache.org/list.html?dev@logging.apache.org</archive>
+      <otherArchives>
+        <otherArchive>http://mail-archives.apache.org/mod_mbox/logging-dev/</otherArchive>
+        <otherArchive>http://marc.info/?l=dev</otherArchive>
+        <otherArchive>http://dir.gmane.org/gmane.comp.jakarta.log4j.devel</otherArchive>
+      </otherArchives>
+    </mailingList>
+  </mailingLists>
+  <scm>
+    <connection>scm:git:https://gitbox.apache.org/repos/asf/logging-log4j2.git</connection>
+    <developerConnection>scm:git:https://gitbox.apache.org/repos/asf/logging-log4j2.git</developerConnection>
+    <url>https://gitbox.apache.org/repos/asf?p=logging-log4j2.git</url>
+    <tag>log4j-${Log4jReleaseVersion}</tag>
+  </scm>
+  <properties>
+    <!-- make sure to update these for each release! -->
+    <log4jParentDir>${basedir}</log4jParentDir>
+    <Log4jReleaseVersion>2.10.0</Log4jReleaseVersion>
+    <Log4jReleaseManager>Ralph Goers</Log4jReleaseManager>
+    <Log4jReleaseKey>B3D8E1BA</Log4jReleaseKey>
+    <!--<Log4jReleaseManager>Matt Sicker</Log4jReleaseManager> -->
+    <!--<Log4jReleaseKey>748F15B2CF9BA8F024155E6ED7C92B70FA1C814D</Log4jReleaseKey> -->
+    <!-- note that any properties you want available in velocity templates must not use periods! -->
+    <slf4jVersion>1.7.25</slf4jVersion>
+    <logbackVersion>1.2.3</logbackVersion>
+    <jackson1Version>1.9.13</jackson1Version>
+    <jackson2Version>2.11.1</jackson2Version>
+    <springVersion>3.2.18.RELEASE</springVersion>
+    <kubernetes-client.version>4.6.1</kubernetes-client.version>
+    <flumeVersion>1.9.0</flumeVersion>
+    <disruptorVersion>3.4.2</disruptorVersion>
+    <conversantDisruptorVersion>1.2.15</conversantDisruptorVersion>
+    <elastic.version>7.6.2</elastic.version>
+    <mongodb3.version>3.12.4</mongodb3.version>
+    <mongodb4.version>4.0.3</mongodb4.version>
+    <groovy.version>2.5.6</groovy.version>
+    <compiler.plugin.version>3.8.1</compiler.plugin.version>
+    <pmd.plugin.version>3.10.0</pmd.plugin.version>
+    <spotbugs.plugin.version>3.1.5</spotbugs.plugin.version>
+    <spotbugs.version>3.1.7</spotbugs.version>
+    <changes.plugin.version>2.12.1</changes.plugin.version>
+    <javadoc.plugin.version>3.2.0</javadoc.plugin.version>
+    <!-- surefire.plugin.version 2.18 yields http://jira.codehaus.org/browse/SUREFIRE-1121, which is fixed in 2.18.1 -->
+    <!-- surefire.plugin.version 2.19 yields https://issues.apache.org/jira/browse/SUREFIRE-1193. -->
+    <!-- all versions after 2.13 yield https://issues.apache.org/jira/browse/SUREFIRE-720 -->
+    <surefire.plugin.version>2.21.0</surefire.plugin.version>
+    <failsafe.plugin.version>2.21.0</failsafe.plugin.version>
+    <checkstyle.plugin.version>3.0.0</checkstyle.plugin.version>
+    <deploy.plugin.version>2.8.2</deploy.plugin.version>
+    <rat.plugin.version>0.13</rat.plugin.version>
+    <pdf.plugin.version>1.2</pdf.plugin.version>
+    <cobertura.plugin.version>2.7</cobertura.plugin.version>
+    <jacoco.plugin.version>0.8.5</jacoco.plugin.version>
+    <release.plugin.version>2.5.3</release.plugin.version>
+    <scm.plugin.version>1.9.5</scm.plugin.version>
+    <jxr.plugin.version>2.5</jxr.plugin.version>
+    <revapi.plugin.version>0.10.5</revapi.plugin.version>
+    <revapi.skip>true</revapi.skip>
+    <site.plugin.version>3.8.2</site.plugin.version>
+    <!-- Maven site depends on Velocity and the escaping rules are different in newer versions. -->
+    <!-- See https://maven.apache.org/plugins/maven-site-plugin/migrate.html -->
+    <velocity.plugin.version>1.5</velocity.plugin.version>
+    <asciidoc.plugin.version>1.5.6</asciidoc.plugin.version>
+    <errorprone.version>2.3.2</errorprone.version>
+    <plexus.errorprone.version>2.8.5</plexus.errorprone.version>
+    <remote.resources.plugin.version>1.5</remote.resources.plugin.version>
+    <manifestfile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestfile>
+    <maven.compiler.source>1.8</maven.compiler.source>
+    <maven.compiler.target>1.8</maven.compiler.target>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <docLabel>Site Documentation</docLabel>
+    <projectDir />
+    <commonsLoggingVersion>1.2</commonsLoggingVersion>
+    <javax.persistence>2.2.1</javax.persistence>
+    <osgi.api.version>6.0.0</osgi.api.version>
+    <activemq.version>5.15.9</activemq.version>
+    <!-- Allow Clirr severity to be overriden by the command-line option -DminSeverity=level -->
+    <minSeverity>info</minSeverity>
+    <jctoolsVersion>1.2.1</jctoolsVersion>
+    <mockitoVersion>2.25.1</mockitoVersion>
+    <argLine>-Xms256m -Xmx1024m</argLine>
+    <javaTargetVersion>1.8</javaTargetVersion>
+    <module.name />
+  </properties>
+  <pluginRepositories>
+    <pluginRepository>
+      <id>apache</id>
+      <url>https://repository.apache.org/content/repositories/releases/</url>
+    </pluginRepository>
+<!--     <pluginRepository> -->
+<!--       <id>apache.snapshots</id> -->
+<!--       <name>Apache snapshots repository</name> -->
+<!--       <url>http://repository.apache.org/content/groups/snapshots</url> -->
+<!--       <snapshots> -->
+<!--         <enabled>true</enabled> -->
+<!--       </snapshots> -->
+<!--     </pluginRepository>     -->
+  </pluginRepositories>
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-api</artifactId>
+        <version>${slf4jVersion}</version>
+      </dependency>
+       <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-ext</artifactId>
+        <version>${slf4jVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>ch.qos.logback</groupId>
+        <artifactId>logback-core</artifactId>
+        <version>${logbackVersion}</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>ch.qos.logback</groupId>
+        <artifactId>logback-core</artifactId>
+        <type>test-jar</type>
+        <version>${logbackVersion}</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.eclipse.tycho</groupId>
+        <artifactId>org.eclipse.osgi</artifactId>
+        <version>3.12.1.v20170821-1548</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>org.apache.felix.framework</artifactId>
+        <version>5.6.10</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.maven</groupId>
+        <artifactId>maven-core</artifactId>
+        <version>3.6.0</version>
+      </dependency>
+      <dependency>
+        <groupId>commons-codec</groupId>
+        <artifactId>commons-codec</artifactId>
+        <version>1.12</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-lang3</artifactId>
+        <version>3.9</version>
+      </dependency>
+      <dependency>
+        <groupId>ch.qos.logback</groupId>
+        <artifactId>logback-classic</artifactId>
+        <version>${logbackVersion}</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>ch.qos.logback</groupId>
+        <artifactId>logback-classic</artifactId>
+        <version>${logbackVersion}</version>
+        <type>test-jar</type>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-api-java9</artifactId>
+        <version>${project.version}</version>
+        <type>zip</type>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-api</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-plugins-java9</artifactId>
+        <version>${project.version}</version>
+        <type>zip</type>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-plugins</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-1.2-api</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-plugins</artifactId>
+        <version>${project.version}</version>
+        <type>test-jar</type>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-api</artifactId>
+        <version>${project.version}</version>
+        <type>test-jar</type>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-core-java9</artifactId>
+        <version>${project.version}</version>
+        <type>zip</type>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-core</artifactId>
+        <version>${project.version}</version>
+      </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.apache.logging.log4j</groupId>
+        <artifactId>log4j-layout-json-template</artifactId>
+        <version>${project.version}</version>
+        <type>test-jar</type>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-slf4j-impl</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-slf4j-impl</artifactId>
+        <version>${project.version}</version>
+        <type>zip</type>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-slf4j18-impl</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-jcl</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>commons-logging</groupId>
+        <artifactId>commons-logging</artifactId>
+        <version>${commonsLoggingVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-flume-ng</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-iostreams</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-jul</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-jpl</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-taglib</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-web</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>com.sleepycat</groupId>
+        <artifactId>je</artifactId>
+        <version>5.0.73</version>
+      </dependency>
+      <dependency>
+        <groupId>org.osgi</groupId>
+        <artifactId>org.osgi.core</artifactId>
+        <version>${osgi.api.version}</version>
+        <scope>provided</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.fusesource.jansi</groupId>
+        <artifactId>jansi</artifactId>
+        <version>1.17.1</version>
+        <optional>true</optional>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.flume</groupId>
+        <artifactId>flume-ng-sdk</artifactId>
+        <version>${flumeVersion}</version>
+        <exclusions>
+          <exclusion>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-core-asl</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-mapper-asl</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.flume</groupId>
+        <artifactId>flume-ng-core</artifactId>
+        <version>${flumeVersion}</version>
+        <exclusions>
+          <exclusion>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.flume</groupId>
+        <artifactId>flume-ng-embedded-agent</artifactId>
+        <version>${flumeVersion}</version>
+        <exclusions>
+          <exclusion>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-core-asl</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-mapper-asl</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.flume</groupId>
+        <artifactId>flume-ng-node</artifactId>
+        <version>${flumeVersion}</version>
+        <exclusions>
+          <exclusion>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-core-asl</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-mapper-asl</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.flume.flume-ng-channels</groupId>
+        <artifactId>flume-file-channel</artifactId>
+        <version>${flumeVersion}</version>
+        <exclusions>
+          <exclusion>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>org.mortbay.jetty</groupId>
+            <artifactId>servlet-api</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>org.mortbay.jetty</groupId>
+            <artifactId>servlet-api-2.5</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.hadoop</groupId>
+        <artifactId>hadoop-core</artifactId>
+        <version>1.2.1</version>
+        <exclusions>
+          <exclusion>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-core-asl</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>org.codehaus.jackson</groupId>
+            <artifactId>jackson-mapper-asl</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>org.mortbay.jetty</groupId>
+            <artifactId>servlet-api</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <!-- Jackson 1 start -->
+      <dependency>
+        <groupId>org.codehaus.jackson</groupId>
+        <artifactId>jackson-core-asl</artifactId>
+        <version>${jackson1Version}</version>
+        <scope>runtime</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.codehaus.jackson</groupId>
+        <artifactId>jackson-mapper-asl</artifactId>
+        <version>${jackson1Version}</version>
+        <scope>runtime</scope>
+      </dependency>
+      <!-- Jackson 1 end -->
+      <!-- Jackson 2 start -->
+      <dependency>
+        <groupId>com.fasterxml.jackson.core</groupId>
+        <artifactId>jackson-core</artifactId>
+        <version>${jackson2Version}</version>
+        <optional>true</optional>
+      </dependency>
+      <dependency>
+        <groupId>com.fasterxml.jackson.core</groupId>
+        <artifactId>jackson-databind</artifactId>
+        <version>${jackson2Version}</version>
+        <optional>true</optional>
+      </dependency>
+      <dependency>
+        <groupId>com.fasterxml.jackson.core</groupId>
+        <artifactId>jackson-annotations</artifactId>
+        <version>${jackson2Version}</version>
+        <optional>true</optional>
+      </dependency>
+      <dependency>
+        <groupId>com.fasterxml.jackson.dataformat</groupId>
+        <artifactId>jackson-dataformat-yaml</artifactId>
+        <version>${jackson2Version}</version>
+        <optional>true</optional>
+      </dependency>
+      <dependency>
+        <groupId>com.fasterxml.jackson.dataformat</groupId>
+        <artifactId>jackson-dataformat-xml</artifactId>
+        <version>${jackson2Version}</version>
+        <optional>true</optional>
+      </dependency>
+      <dependency>
+        <groupId>com.fasterxml.jackson.module</groupId>
+        <artifactId>jackson-module-jaxb-annotations</artifactId>
+        <version>${jackson2Version}</version>
+        <optional>true</optional>
+      </dependency>
+      <!-- Jackson 2 end -->
+      <dependency>
+        <groupId>com.sun.mail</groupId>
+        <artifactId>javax.mail</artifactId>
+        <version>1.6.2</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jboss.spec.javax.jms</groupId>
+        <artifactId>jboss-jms-api_1.1_spec</artifactId>
+        <version>1.0.1.Final</version>
+        <scope>provided</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.activemq</groupId>
+        <artifactId>activemq-broker</artifactId>
+        <version>${activemq.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.kafka</groupId>
+        <artifactId>kafka-clients</artifactId>
+        <version>1.1.1</version>
+      </dependency>
+      <dependency>
+        <groupId>org.zeromq</groupId>
+        <artifactId>jeromq</artifactId>
+        <version>0.4.3</version>
+      </dependency>
+      <dependency>
+        <groupId>javax.servlet</groupId>
+        <artifactId>servlet-api</artifactId>
+        <version>2.5</version>
+        <scope>provided</scope>
+      </dependency>
+      <dependency>
+        <groupId>com.lmax</groupId>
+        <artifactId>disruptor</artifactId>
+        <version>${disruptorVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>com.conversantmedia</groupId>
+        <artifactId>disruptor</artifactId>
+        <version>${conversantDisruptorVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.jctools</groupId>
+        <artifactId>jctools-core</artifactId>
+        <version>${jctoolsVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>junit</groupId>
+        <artifactId>junit</artifactId>
+        <version>4.12</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.assertj</groupId>
+        <artifactId>assertj-core</artifactId>
+        <version>3.14.0</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.hamcrest</groupId>
+        <artifactId>hamcrest-all</artifactId>
+        <version>1.3</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.awaitility</groupId>
+        <artifactId>awaitility</artifactId>
+        <version>4.0.2</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.codehaus.plexus</groupId>
+        <artifactId>plexus-utils</artifactId>
+        <version>3.2.0</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.mockito</groupId>
+        <artifactId>mockito-core</artifactId>
+        <version>${mockitoVersion}</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-aop</artifactId>
+        <version>${springVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-beans</artifactId>
+        <version>${springVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-context</artifactId>
+        <version>${springVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-core</artifactId>
+        <version>${springVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-expression</artifactId>
+        <version>${springVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-oxm</artifactId>
+        <version>${springVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-test</artifactId>
+        <version>${springVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-web</artifactId>
+        <version>${springVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework</groupId>
+        <artifactId>spring-webmvc</artifactId>
+        <version>${springVersion}</version>
+      </dependency>
+      <dependency>
+        <groupId>io.fabric8</groupId>
+        <artifactId>kubernetes-client</artifactId>
+        <version>${kubernetes-client.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.hsqldb</groupId>
+        <artifactId>hsqldb</artifactId>
+        <version>2.4.1</version>
+      </dependency>
+      <dependency>
+        <groupId>com.h2database</groupId>
+        <artifactId>h2</artifactId>
+        <version>1.4.199</version>
+      </dependency>
+      <dependency>
+        <groupId>org.eclipse.persistence</groupId>
+        <artifactId>org.eclipse.persistence.jpa</artifactId>
+        <version>2.7.4</version>
+      </dependency>
+      <dependency>
+        <groupId>org.eclipse.persistence</groupId>
+        <artifactId>javax.persistence</artifactId>
+        <version>${javax.persistence}</version>
+        <scope>provided</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.lightcouch</groupId>
+        <artifactId>lightcouch</artifactId>
+        <version>0.0.6</version>
+      </dependency>
+      <dependency>
+        <groupId>com.datastax.cassandra</groupId>
+        <artifactId>cassandra-driver-core</artifactId>
+        <version>3.1.4</version>
+      </dependency>
+      <dependency>
+        <groupId>org.liquibase</groupId>
+        <artifactId>liquibase-core</artifactId>
+        <version>3.5.3</version>
+      </dependency>
+      <dependency>
+        <groupId>net.javacrumbs.json-unit</groupId>
+        <artifactId>json-unit</artifactId>
+        <version>1.31.1</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.xmlunit</groupId>
+        <artifactId>xmlunit-core</artifactId>
+        <version>2.6.2</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.xmlunit</groupId>
+        <artifactId>xmlunit-matchers</artifactId>
+        <version>2.6.2</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>commons-io</groupId>
+        <artifactId>commons-io</artifactId>
+        <version>2.6</version>
+        <scope>test</scope>
+      </dependency>
+      <!-- Used for testing JsonTemplateLayout -->
+      <dependency>
+        <groupId>co.elastic.logging</groupId>
+        <artifactId>log4j2-ecs-layout</artifactId>
+        <version>0.4.0</version>
+      </dependency>
+      <dependency>
+        <groupId>org.elasticsearch.client</groupId>
+        <artifactId>elasticsearch-rest-high-level-client</artifactId>
+        <version>${elastic.version}</version>
+      </dependency>
+      <!-- Used for testing HttpAppender -->
+      <dependency>
+        <groupId>com.github.tomakehurst</groupId>
+        <artifactId>wiremock</artifactId>
+        <scope>test</scope>
+        <version>2.19.0</version>
+      </dependency>
+      <!-- Used for compressing to formats other than zip and gz -->
+      <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-compress</artifactId>
+        <version>1.18</version>
+      </dependency>
+      <dependency>
+        <groupId>org.tukaani</groupId>
+        <artifactId>xz</artifactId>
+        <version>1.8</version>
+        <scope>test</scope>
+      </dependency>
+      <!-- Used for the CSV layout -->
+      <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-csv</artifactId>
+        <version>1.6</version>
+      </dependency>
+      <!-- GC-free -->
+      <dependency>
+        <groupId>com.google.code.java-allocation-instrumenter</groupId>
+        <artifactId>java-allocation-instrumenter</artifactId>
+        <version>3.0.1</version>
+      </dependency>
+      <dependency>
+        <groupId>org.hdrhistogram</groupId>
+        <artifactId>HdrHistogram</artifactId>
+        <version>2.1.9</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache-extras.beanshell</groupId>
+        <artifactId>bsh</artifactId>
+        <version>2.0b6</version>
+      </dependency>
+      <dependency>
+        <groupId>org.codehaus.groovy</groupId>
+        <artifactId>groovy-jsr223</artifactId>
+        <version>${groovy.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.codehaus.groovy</groupId>
+        <artifactId>groovy-dateutil</artifactId>
+        <version>${groovy.version}</version>
+      </dependency>
+      <dependency>
+        <!-- Testing MongoDB -->
+        <groupId>de.flapdoodle.embed</groupId>
+        <artifactId>de.flapdoodle.embed.mongo</artifactId>
+        <version>2.2.0</version>
+        <scope>test</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+  <build>
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <groupId>org.apache.felix</groupId>
+          <artifactId>maven-bundle-plugin</artifactId>
+          <version>3.5.0</version>
+          <inherited>true</inherited>
+          <extensions>true</extensions>
+          <executions>
+            <execution>
+              <goals>
+                <goal>manifest</goal>
+              </goals>
+              <phase>process-classes</phase>
+            </execution>
+          </executions>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-changes-plugin</artifactId>
+          <version>${changes.plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-release-plugin</artifactId>
+          <version>${release.plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-scm-plugin</artifactId>
+          <version>${scm.plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-checkstyle-plugin</artifactId>
+          <version>${checkstyle.plugin.version}</version>
+        </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>
+            <additionalparam>${javadoc.opts}</additionalparam>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-pmd-plugin</artifactId>
+          <version>${pmd.plugin.version}</version>
+        </plugin>
+        <!-- some nice default compiler options -->
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>${compiler.plugin.version}</version>
+          <configuration>
+            <source>${maven.compiler.source}</source>
+            <target>${maven.compiler.target}</target>
+            <showDeprecation>true</showDeprecation>
+            <showWarnings>true</showWarnings>
+            <encoding>UTF-8</encoding>
+            <fork>true</fork>
+            <meminitial>256</meminitial>
+            <maxmem>1024</maxmem>
+            <compilerArguments>
+              <Xmaxwarns>10000</Xmaxwarns>
+              <Xlint />
+            </compilerArguments>
+            <compilerId>javac-with-errorprone</compilerId>
+            <forceJavacCompilerUse>true</forceJavacCompilerUse>
+            <parameters>true</parameters>
+          </configuration>
+          <dependencies>
+            <dependency>
+              <groupId>org.codehaus.plexus</groupId>
+              <artifactId>plexus-compiler-javac-errorprone</artifactId>
+              <version>${plexus.errorprone.version}</version>
+            </dependency>
+            <dependency>
+              <groupId>com.google.errorprone</groupId>
+              <artifactId>error_prone_core</artifactId>
+              <version>${errorprone.version}</version>
+            </dependency>
+          </dependencies>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <version>${surefire.plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-failsafe-plugin</artifactId>
+          <version>${failsafe.plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-source-plugin</artifactId>
+          <version>3.0.1</version>
+          <executions>
+            <execution>
+              <id>attach-sources</id>
+              <phase>verify</phase>
+              <goals>
+                <goal>jar-no-fork</goal>
+                <goal>test-jar-no-fork</goal>
+              </goals>
+            </execution>
+          </executions>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-jxr-plugin</artifactId>
+          <version>${jxr.plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.eluder.coveralls</groupId>
+          <artifactId>coveralls-maven-plugin</artifactId>
+          <version>4.3.0</version>
+        </plugin>
+        <plugin>
+          <groupId>org.jacoco</groupId>
+          <artifactId>jacoco-maven-plugin</artifactId>
+          <version>${jacoco.plugin.version}</version>
+          <executions>
+            <execution>
+                <id>prepare-agent</id>
+                <goals>
+                  <goal>prepare-agent</goal>
+                </goals>
+            </execution>
+            <execution>
+              <id>default-report</id>
+              <phase>prepare-package</phase>
+              <goals>
+                <goal>report</goal>
+              </goals>
+            </execution>
+          </executions>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-assembly-plugin</artifactId>
+          <version>3.1.0</version>
+        </plugin>
+        <plugin>
+          <groupId>com.github.spotbugs</groupId>
+          <artifactId>spotbugs-maven-plugin</artifactId>
+          <version>${spotbugs.plugin.version}</version>
+          <dependencies>
+            <!-- overwrite dependency on spotbugs if you want to specify the version of spotbugs -->
+            <dependency>
+              <groupId>com.github.spotbugs</groupId>
+              <artifactId>spotbugs</artifactId>
+              <version>${spotbugs.version}</version>
+            </dependency>
+          </dependencies>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-jar-plugin</artifactId>
+          <version>3.1.0</version>
+          <executions>
+            <execution>
+              <id>default-jar</id>
+              <goals>
+                <goal>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>
+                    <Automatic-Module-Name>${module.name}</Automatic-Module-Name>
+                  </manifestEntries>
+                </archive>
+              </configuration>
+            </execution>
+          </executions>
+        </plugin>
+        <plugin>
+          <groupId>io.fabric8</groupId>
+          <artifactId>docker-maven-plugin</artifactId>
+          <version>0.33.0</version>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <artifactId>maven-clean-plugin</artifactId>
+        <version>3.1.0</version>
+      </plugin>
+      <plugin>
+        <artifactId>maven-resources-plugin</artifactId>
+        <version>3.0.2</version>
+        <executions>
+          <execution>
+            <id>copy-sitecss</id>
+            <!-- fetch site.xml before creating site documentation -->
+            <phase>pre-site</phase>
+            <goals>
+              <goal>copy-resources</goal>
+            </goals>
+            <configuration>
+              <outputDirectory>${project.build.directory}/site</outputDirectory>
+              <resources>
+                <resource>
+                  <directory>${log4jParentDir}/src/site/resources</directory>
+                  <includes>
+                    <include>**/*</include>
+                  </includes>
+                </resource>
+              </resources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <version>${surefire.plugin.version}</version>
+        <configuration>
+          <systemPropertyVariables>
+            <java.awt.headless>true</java.awt.headless>
+          </systemPropertyVariables>
+          <forkCount>1</forkCount>
+          <reuseForks>false</reuseForks>
+          <excludes>
+            <exclude>${log4j.skip.test1}</exclude>
+            <exclude>${log4j.skip.test2}</exclude>
+          </excludes>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-failsafe-plugin</artifactId>
+        <version>${failsafe.plugin.version}</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>integration-test</goal>
+              <goal>verify</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <systemPropertyVariables>
+            <java.awt.headless>true</java.awt.headless>
+          </systemPropertyVariables>
+          <argLine>-Xms256m -Xmx1024m</argLine>
+          <forkCount>1</forkCount>
+          <reuseForks>false</reuseForks>
+          <encoding>UTF-8</encoding>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <version>1.12</version>
+        <executions>
+          <execution>
+            <id>timestamp-property</id>
+            <goals>
+              <goal>timestamp-property</goal>
+            </goals>
+            <phase>pre-site</phase>
+            <configuration>
+              <name>currentYear</name>
+              <pattern>yyyy</pattern>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-site-plugin</artifactId>
+        <version>${site.plugin.version}</version>
+        <dependencies>
+          <dependency>
+            <groupId>org.apache.maven.wagon</groupId>
+            <artifactId>wagon-ssh</artifactId>
+            <version>3.1.0</version>
+          </dependency>
+          <dependency>
+            <groupId>org.asciidoctor</groupId>
+            <artifactId>asciidoctor-maven-plugin</artifactId>
+            <version>${asciidoc.plugin.version}</version>
+          </dependency>
+        </dependencies>
+        <configuration>
+          <!-- only build English site even on other language OS -->
+          <locales>en</locales>
+          <!-- Exclude the navigation file for Maven 1 sites
+               and the changes file used by the changes-plugin,
+               as they interfere with the site generation. -->
+          <moduleExcludes>
+            <xdoc>navigation.xml,changes.xml</xdoc>
+          </moduleExcludes>
+          <asciidoc>
+            <attributes>
+              <!-- copy any site properties wanted in asciidoc files -->
+              <Log4jReleaseVersion>${Log4jReleaseVersion}</Log4jReleaseVersion>
+              <Log4jReleaseManager>${Log4jReleaseManager}</Log4jReleaseManager>
+              <Log4jReleaseKey>${Log4jReleaseKey}</Log4jReleaseKey>
+            </attributes>
+          </asciidoc>
+        </configuration>
+      </plugin>
+      <!-- <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>cobertura-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>clean</id>
+            <goals>
+              <goal>clean</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin> -->
+      <!-- We need to disable the standard ASF configuration to be able to publish our own notice and license files -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-remote-resources-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>process</goal>
+            </goals>
+            <configuration>
+              <skip>true</skip>
+              <resourceBundles />
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-pdf-plugin</artifactId>
+        <version>${pdf.plugin.version}</version>
+        <executions>
+          <execution>
+            <id>pdf</id>
+            <phase>site</phase>
+            <goals>
+              <goal>pdf</goal>
+            </goals>
+            <configuration>
+              <outputDirectory>${project.reporting.outputDirectory}</outputDirectory>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+      </plugin>
+      <!-- RAT report -->
+      <plugin>
+        <groupId>org.apache.rat</groupId>
+        <artifactId>apache-rat-plugin</artifactId>
+        <version>${rat.plugin.version}</version>
+        <configuration>
+          <excludes>
+            <!-- Matches other RAT configurations in this POM -->
+            <exclude>src/main/resources/META-INF/services/**/*</exclude>
+            <!-- IntelliJ files -->
+            <exclude>.idea/**/*</exclude>
+            <exclude>src/test/resources/**/*</exclude>
+            <!-- IDE settings imports -->
+            <exclude>src/ide/**</exclude>
+            <!-- does it even make sense to apply a license to a GPG signature? -->
+            <exclude>**/*.asc</exclude>
+            <!-- jQuery is MIT-licensed, but RAT can't figure it out -->
+            <exclude>src/site/resources/js/jquery.js</exclude>
+            <exclude>src/site/resources/js/jquery.min.js</exclude>
+            <!-- highlight.js is BSD3-licensed -->
+            <exclude>src/site/resources/js/highlight.pack.js</exclude>
+            <!-- Generated files -->
+            <exclude>log4j-distribution/target/**/*</exclude>
+            <exclude>log4j-distribution/.project</exclude>
+            <exclude>log4j-distribution/.settings/**</exclude>
+            <exclude>velocity.log</exclude>
+            <!-- Other -->
+            <exclude>felix-cache/**</exclude>
+            <exclude>RELEASE-NOTES.md</exclude>
+            <exclude>**/*.yml</exclude>
+            <exclude>**/*.yaml</exclude>
+            <exclude>**/*.json</exclude>
+            <excllude>**/images/*.drawio</excllude>
+            <exclude>**/fluent-bit.conf</exclude>
+            <exclude>**/rabbitmq.config</exclude>
+            <exclude>**/MANIFEST.MF</exclude>
+          </excludes>
+        </configuration>
+      </plugin>
+      <!-- DOAP (RDF) metadata generation -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-doap-plugin</artifactId>
+        <version>1.2</version>
+        <configuration>
+          <doapOptions>
+            <programmingLanguage>Java</programmingLanguage>
+            <category>library</category>
+          </doapOptions>
+          <asfExtOptions>
+            <charter>
+              The Apache Logging Services Project creates and maintains open-source software related to the logging of
+              application behavior and released at no charge to the public.
+            </charter>
+            <pmc>https://logging.apache.org</pmc>
+          </asfExtOptions>
+        </configuration>
+        <executions>
+          <execution>
+            <id>site</id>
+            <phase>site</phase>
+            <goals>
+              <goal>generate</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.revapi</groupId>
+        <artifactId>revapi-maven-plugin</artifactId>
+        <version>${revapi.plugin.version}</version>
+        <dependencies>
+          <dependency>
+            <groupId>org.revapi</groupId>
+            <artifactId>revapi-java</artifactId>
+            <version>0.18.2</version>
+          </dependency>
+        </dependencies>
+        <executions>
+          <execution>
+            <goals><goal>check</goal></goals>
+            <configuration>
+              <checkDependencies>false</checkDependencies>
+              <skip>${revapi.skip}</skip>
+              <failOnMissingConfigurationFiles>false</failOnMissingConfigurationFiles>
+              <analysisConfigurationFiles>
+                <path>revapi.json</path>
+              </analysisConfigurationFiles>
+              <analysisConfiguration><![CDATA[
+[
+  {
+     "extension": "revapi.java",
+     "configuration": {
+       "missing-classes": {
+         "behavior": "report",
+         "ignoreMissingAnnotations": false
+       },
+       "reportUsesFor": [
+          "java.missing.newClass",
+          "java.class.nonPublicPartOfAPI"
+       ],
+       "filter": {
+         "classes": {
+           "regex": true,
+           "include": [
+             "org\\.apache\\.logging\\.log4j(\\..+)?"
+           ]
+         },
+         "packages": {
+           "regex": true,
+           "include": [
+             "org\\.apache\\.logging\\.log4j(\\..+)?"
+           ]
+         }
+       }
+     }
+  }
+]
+              ]]></analysisConfiguration>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+  <reporting>
+    <plugins>
+      <!-- Changes report -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-changes-plugin</artifactId>
+        <version>${changes.plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <reports>
+              <report>changes-report</report>
+              <report>jira-report</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+        <configuration>
+          <statusIds>Resolved, Closed</statusIds>
+          <columnNames>Type,Key,Summary,Assignee,Status,Resolution,Fix Version</columnNames>
+          <useJql>true</useJql>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-project-info-reports-plugin</artifactId>
+        <version>2.9</version>
+        <reportSets>
+          <reportSet>
+            <reports>
+              <report>index</report>
+              <report>dependencies</report>
+              <report>dependency-info</report>
+              <report>dependency-convergence</report>
+              <report>dependency-management</report>
+              <report>project-team</report>
+              <report>mailing-list</report>
+              <report>issue-tracking</report>
+              <report>license</report>
+              <report>scm</report>
+              <report>summary</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+        <configuration>
+          <!-- you'd think these would be the defaults, right? -->
+          <customBundle>${project.basedir}/src/site/custom/project-info-report.properties</customBundle>
+          <webAccessUrl>${project.scm.url}</webAccessUrl>
+          <anonymousConnection>${project.scm.connection}</anonymousConnection>
+          <developerConnection>${project.scm.developerConnection}</developerConnection>
+          <scmTag>log4j-${Log4jReleaseVersion}</scmTag>
+        </configuration>
+      </plugin>
+      <!-- Surefire report -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-report-plugin</artifactId>
+        <version>${surefire.plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <id>integration-tests</id>
+            <reports>
+              <report>failsafe-report-only</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+      </plugin>
+      <!-- RAT report -->
+      <plugin>
+        <groupId>org.apache.rat</groupId>
+        <artifactId>apache-rat-plugin</artifactId>
+        <version>${rat.plugin.version}</version>
+        <configuration>
+          <excludes>
+            <!-- Matches other RAT configurations in this POM -->
+            <exclude>src/main/resources/META-INF/services/**/*</exclude>
+            <!-- IntelliJ files -->
+            <exclude>.idea/**/*</exclude>
+            <exclude>src/test/resources/**/*</exclude>
+            <!-- IDE settings imports -->
+            <exclude>src/ide/**</exclude>
+            <!-- does it even make sense to apply a license to a GPG signature? -->
+            <exclude>**/*.asc</exclude>
+            <!-- jQuery is MIT-licensed, but RAT can't figure it out -->
+            <exclude>src/site/resources/js/jquery.js</exclude>
+            <exclude>src/site/resources/js/jquery.min.js</exclude>
+            <!-- highlight.js is BSD3-licensed -->
+            <exclude>src/site/resources/js/highlight.pack.js</exclude>
+            <!-- Generated files -->
+            <exclude>log4j-distribution/target/**/*</exclude>
+            <exclude>log4j-distribution/.project</exclude>
+            <exclude>log4j-distribution/.settings/**</exclude>
+            <exclude>velocity.log</exclude>
+            <!-- Other -->
+            <exclude>felix-cache/**</exclude>
+            <exclude>RELEASE-NOTES.txt</exclude>
+            <exclude>**/*.yml</exclude>
+            <exclude>**/*.yaml</exclude>
+            <exclude>**/*.json</exclude>
+            <excllude>**/images/*.drawio</excllude>
+            <exclude>**/fluent-bit.conf</exclude>
+            <exclude>**/rabbitmq.config</exclude>
+            <exclude>**/MANIFEST.MF</exclude>
+          </excludes>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.revapi</groupId>
+        <artifactId>revapi-maven-plugin</artifactId>
+        <version>${revapi.plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <reports>
+              <report>report</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+      </plugin>
+    </plugins>
+  </reporting>
+  <distributionManagement>
+    <downloadUrl>https://logging.apache.org/log4j/2.x/download.html</downloadUrl>
+    <!-- site is only included to make maven-site-plugin stop complaining -->
+    <site>
+      <id>www.example.com</id>
+      <url>scp://www.example.com/www/docs/project/</url>
+    </site>
+  </distributionManagement>
+  <modules>
+    <module>log4j-api-java9</module>
+    <module>log4j-api</module>
+    <module>log4j-plugins-java9</module>
+    <module>log4j-plugins</module>
+    <module>log4j-core-java9</module>
+    <module>log4j-core</module>
+    <module>log4j-layout-jackson</module>
+    <module>log4j-layout-jackson-json</module>
+    <module>log4j-layout-jackson-xml</module>
+    <module>log4j-layout-jackson-yaml</module>
+    <module>log4j-layout-json-template</module>
+    <module>log4j-core-its</module>
+    <module>log4j-1.2-api</module>
+    <module>log4j-slf4j-impl</module>
+    <module>log4j-slf4j18-impl</module>
+    <module>log4j-to-slf4j</module>
+    <module>log4j-jcl</module>
+    <module>log4j-csv</module>
+    <module>log4j-flume-ng</module>
+    <module>log4j-taglib</module>
+    <module>log4j-jmx-gui</module>
+    <module>log4j-samples</module>
+    <module>log4j-bom</module>
+    <module>log4j-jdbc</module>
+    <module>log4j-jdbc-dbcp2</module>
+    <module>log4j-jpa</module>
+    <module>log4j-jeromq</module>
+    <module>log4j-jms</module>
+    <module>log4j-kafka</module>
+    <module>log4j-couchdb</module>
+    <module>log4j-mongodb3</module>
+    <module>log4j-mongodb4</module>
+    <module>log4j-cassandra</module>
+    <module>log4j-web</module>
+    <module>log4j-perf</module>
+    <module>log4j-iostreams</module>
+    <module>log4j-jul</module>
+    <module>log4j-jpl</module>
+    <module>log4j-liquibase</module>
+    <module>log4j-appserver</module>
+    <module>log4j-smtp</module>
+    <module>log4j-osgi</module>
+    <module>log4j-docker</module>
+    <module>log4j-kubernetes</module>
+    <module>log4j-spring-cloud-config</module>
+  </modules>
+  <profiles>
+    <profile>
+      <id>pdf</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-pdf-plugin</artifactId>
+            <version>${pdf.plugin.version}</version>
+            <executions>
+              <execution>
+                <id>pdf</id>
+                <phase>generate-resources</phase>
+                <goals>
+                  <goal>pdf</goal>
+                </goals>
+                <configuration>
+                  <outputDirectory>${project.reporting.outputDirectory}</outputDirectory>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+    <profile>
+      <id>release-notes</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-changes-plugin</artifactId>
+            <version>${changes.plugin.version}</version>
+            <configuration>
+              <template>announcement.vm</template>
+              <templateDirectory>src/changes</templateDirectory>
+              <runOnlyAtExecutionRoot>true</runOnlyAtExecutionRoot>
+              <announcementDirectory>.</announcementDirectory>
+              <announcementFile>RELEASE-NOTES.md</announcementFile>
+              <issueManagementSystems>
+                <issueManagementSystem>changes.xml</issueManagementSystem>
+                <!--<issueManagementSystem>JIRA</issueManagementSystem> -->
+              </issueManagementSystems>
+              <version>${Log4jReleaseVersion}</version>
+              <announceParameters>
+                <releaseVersion>${Log4jReleaseVersion}</releaseVersion>
+                <releaseCount>${Log4jReleaseCount}</releaseCount>
+              </announceParameters>
+              <useJql>true</useJql>
+            </configuration>
+            <executions>
+              <execution>
+                <id>create-release-notes</id>
+                <phase>generate-resources</phase>
+                <goals>
+                  <goal>announcement-generate</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+    <profile>
+      <id>apache-release</id>
+      <build>
+        <plugins>
+          <plugin>
+            <artifactId>maven-assembly-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>source-release-assembly</id>
+                <configuration>
+                  <skipAssembly>true</skipAssembly>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+      <modules>
+        <module>log4j-distribution</module>
+      </modules>
+    </profile>
+    <profile>
+      <id>rat</id>
+      <build>
+        <plugins>
+          <!-- RAT report -->
+          <plugin>
+            <groupId>org.apache.rat</groupId>
+            <artifactId>apache-rat-plugin</artifactId>
+            <version>${rat.plugin.version}</version>
+            <configuration>
+              <excludes>
+                <!-- Matches other RAT configurations in this POM -->
+                <exclude>src/main/resources/META-INF/services/**/*</exclude>
+                <!-- IntelliJ files -->
+                <exclude>.idea/**/*</exclude>
+                <exclude>src/test/resources/**/*</exclude>
+                <!-- IDE settings imports -->
+                <exclude>src/ide/**</exclude>
+                <!-- does it even make sense to apply a license to a GPG signature? -->
+                <exclude>**/*.asc</exclude>
+                <!-- jQuery is MIT-licensed, but RAT can't figure it out -->
+                <exclude>src/site/resources/js/jquery.js</exclude>
+                <exclude>src/site/resources/js/jquery.min.js</exclude>
+                <!-- highlight.js is BSD3-licensed -->
+                <exclude>src/site/resources/js/highlight.pack.js</exclude>
+                <!-- Generated files -->
+                <exclude>log4j-distribution/target/**/*</exclude>
+                <exclude>log4j-distribution/.project</exclude>
+                <exclude>log4j-distribution/.settings/**</exclude>
+                <exclude>velocity.log</exclude>
+                <!-- Other -->
+                <exclude>felix-cache/**</exclude>
+                <exclude>RELEASE-NOTES.md</exclude>
+                <exclude>**/*.yml</exclude>
+                <exclude>**/*.yaml</exclude>
+                <exclude>**/*.json</exclude>
+                <excllude>**/images/*.drawio</excllude>
+                <exclude>**/fluent-bit.conf</exclude>
+                <exclude>**/rabbitmq.config</exclude>
+                <exclude>**/MANIFEST.MF</exclude>
+              </excludes>
+            </configuration>
+            <executions>
+              <execution>
+                <phase>verify</phase>
+                <goals>
+                  <goal>check</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+    <profile>
+      <!-- http://www.yourkit.com/docs/80/help/agent.jsp -->
+      <id>yourkit-mac</id>
+      <!--
+      <activation>
+        <os>
+          <family>Mac</family>
+        </os>
+        <file>
+          <exists>${yourkit.home}/bin/mac/libyjpagent.jnilib</exists>
+        </file>
+      </activation>
+      -->
+      <properties>
+        <yourkit.home>/Applications/YJP.app</yourkit.home>
+      </properties>
+      <dependencies>
+        <dependency>
+          <groupId>com.yourkit</groupId>
+          <artifactId>yjp-controller-api-redist</artifactId>
+          <version>2013</version>
+          <scope>system</scope>
+          <systemPath>${yourkit.home}/lib/yjp-controller-api-redist.jar</systemPath>
+        </dependency>
+      </dependencies>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-surefire-plugin</artifactId>
+            <configuration>
+              <argLine>-agentpath:"${yourkit.home}/bin/mac/libyjpagent.jnilib"</argLine>
+            </configuration>
+          </plugin>
+          <plugin>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <configuration>
+              <argLine>-agentpath:"${yourkit.home}/bin/mac/libyjpagent.jnilib"</argLine>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+    <profile>
+      <id>java8-doclint-disabled</id>
+      <activation>
+        <jdk>[1.8,)</jdk>
+      </activation>
+      <properties>
+        <javadoc.opts>-Xdoclint:none</javadoc.opts>
+      </properties>
+    </profile>
+  </profiles>
+</project>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index bad5986..0f0a246 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -31,6 +31,12 @@
          - "remove" - Removed
     -->
     <release version="3.0.0" date="2019-xx-xx" description="GA Release 3.0.0">
+      <action issue="LOG4J2-2749" dev="vy" type="fix" due-to="Oleksii Khomchenko">
+        JsonLayout KeyValuePair should discard blank JSON keys.
+      </action>
+      <action issue="LOG4J2-2344" dev="vy" type="fix" due-to="dengliming">
+        Fix exception message in PropertiesConfigurationBuilder#createFilter().
+      </action>
       <action issue="LOG4J2-2795" dev="rgoers" type="fix">
         Reduce Log4j 2 initialization time by deferring loading Plugin classes.
       </action>
@@ -167,8 +173,56 @@
       <action issue="LOG4J2-2572" dev="ggregory" type="update">
         Update Apache Flume from 1.8.0 to 1.9.0.
       </action>
+      <action issue="LOG4J2-2844" dev="ggregory" type="fix">
+        Null pointer exception when no network interfaces are available.
+      </action>
     </release>
-    <release version="2.13.2" date="2020-MM-DD" description="GA Release 2.13.2">
+    <release version="2.14.0" date="2020-MM-DD" description="GA Release 2.14.0">
+      <action issue="LOG4J2-2882" dev="rgoers" type="fix" due-to="Emmanuel Bourg">
+        Support java.util.logging filters when using that API.
+      </action>
+      <action issue="LOG4J2-2880" dev="rgoers" type="fix">
+        Create StackWalker benchmark. Revert back to StackWalker.walk based on benchmark results.
+      </action>
+      <action issue="LOG4J2-2867" dev="rgoers" type="fix">
+        Obtain ContextDataProviders asynchronously.
+      </action>
+      <action issue="LOG4J2-2877" dev="rgoers" type="fix">
+        Determine the container id to obtain container and image information.
+      </action>
+      <action issue="LOG4J2-2848" dev="ggregory" type="add">
+        Create module log4j-mongodb4 to use new major version 4 MongoDB driver.
+      </action>
+      <action issue="LOG4J2-2851" dev="ggregory" type="remove">
+        Drop log4j-mongodb2 module.
+      </action>
+      <action issue="LOG4J2-2895" dev="ckozak" type="fix">
+        Fix potential deadlock in asynchronous logging by avoiding blocking for queue space on Log4jThreads
+      </action>
+      <action issue="LOG4J2-2837" dev="ckozak" type="fix">
+        Disruptor and JUL no longer recursively start the AsyncLoggerDisruptor
+        resulting in an extra disruptor background thread constantly waiting.
+      </action>
+      <action issue="LOG4J2-2867" dev="ckozak" type="fix">
+        RingBufferLogEventTranslator uses a static ContextDataInjector instead of initializing a new object
+        on each thread.
+      </action>
+      <action issue="LOG4J2-2858" dev="ckozak" type="add" due-to="Stepan Gorban">
+        More flexible configuration of the Disruptor WaitStrategy.
+      </action>
+      <action issue="LOG4J2-2898" dev="ckozak" type="fix" due-to="Turbanov Andrey">
+        Avoid initializing volatile fields with default values.
+      </action>
+      <action issue="LOG4J2-2899" dev="ckozak" type="fix">
+        Fix log4j-1.2-api LogEventWrapper threadId and priority accessors when called multiple times.
+      </action>
+    </release>
+    <release version="2.13.3" date="2020-05-10" description="GA Release 2.13.3">
+      <action issue="LOG4J2-2838" dev="rgoers" type="fix">
+        Fix NullPointerException in ThreadContextDataInjector.
+      </action>
+    </release>
+    <release version="2.13.2" date="2020-04-23" description="GA Release 2.13.2">
       <action issue="LOG4J2-2824" dev="rgoers" type="fix" due-to="CrazyBills">
         Implement requiresLocation in GelfLayout to reflect whether location information is used in the message Pattern.
       </action>
diff --git a/src/site/asciidoc/javadoc.adoc b/src/site/asciidoc/javadoc.adoc
index 8a0ed75..610bd85 100644
--- a/src/site/asciidoc/javadoc.adoc
+++ b/src/site/asciidoc/javadoc.adoc
@@ -83,12 +83,12 @@
 |link:log4j-liquibase/apidocs/index.html[Log4j Liquibase Binding]
 |The Apache Log4j Liquibase binding to Log4j 2 Core.
 
-|link:log4j-mongodb2/apidocs/index.html[Log4j MongoDB 2 Support]
-|Additional Appender for MongoDB using the version 2 driver.
-
 |link:log4j-mongodb3/apidocs/index.html[Log4j MongoDB 3 Support]
 |Additional Appender for MongoDB using the version 3 driver.
 
+|link:log4j-mongodb4/apidocs/index.html[Log4j MongoDB 4 Support]
+|Additional Appender for MongoDB using the version 4 driver.
+
 |link:log4j-cassandra/apidocs/index.html[Log4j Cassandra Support]
 |Additional Appender for Cassandra.
 |===
diff --git a/src/site/asciidoc/manual/appenders.adoc b/src/site/asciidoc/manual/appenders.adoc
index 430d347..9476517 100644
--- a/src/site/asciidoc/manual/appenders.adoc
+++ b/src/site/asciidoc/manual/appenders.adoc
@@ -1930,126 +1930,16 @@
 [#NoSQLAppenderMongoDB]
 == NoSQLAppenderMongoDB
 
-Starting with Log4 2.11.0, we provide two MongoDB modules:
+We provide the following MongoDB modules:
 
-* `log4j-mongodb2` defines the configuration element
-link:#NoSQLAppenderMongoDB2[`MongoDb2`] matching the MongoDB Driver
-version 2.
-* `log4j-mongodb3` defines the configuration element
+* Added in 2.11.0: `log4j-mongodb3` defines the configuration element
 link:#NoSQLAppenderMongoDB3[`MongoDb3`] matching the MongoDB Driver
 version 3.
+* Added in 2.14.0: `log4j-mongodb4` defines the configuration element
+link:#NoSQLAppenderMongoDB4[`MongoDb4`] matching the MongoDB Driver
+version 4.
 
-We no longer provide the module `log4j-mongodb`.
-
-The module `log4j-mongodb2` aliases the old configuration element
-`MongoDb` to link:#NoSQLAppenderMongoDB2[`MongoDb2`].
-
-[#NoSQLAppenderMongoDB2]
-== NoSQLAppenderMongoDB2
-
-This section details specializations of the
-link:#NoSQLAppender[NoSQLAppender] provider for MongoDB using the
-MongoDB driver version 2. The NoSQLAppender Appender writes log events
-to a NoSQL database using an internal lightweight provider interface.
-
-.MongoDB2 Provider Parameters
-[cols=",,",options="header",]
-|=======================================================================
-|Parameter Name |Type |Description
-|collectionName |String |_Required._ The name of the MongoDB collection
-to insert the events into.
-
-|writeConcernConstant |Field |By default, the MongoDB provider inserts
-records with the instructions `com.mongodb.WriteConcern.ACKNOWLEDGED`.
-Use this optional attribute to specify the name of a constant other than
-`ACKNOWLEDGED`.
-
-|writeConcernConstantClass |Class |If you specify
-`writeConcernConstant`, you can use this attribute to specify a class
-other than `com.mongodb.WriteConcern` to find the constant on (to create
-your own custom instructions).
-
-|factoryClassName |Class |To provide a connection to the MongoDB
-database, you can use this attribute and `factoryMethodName` to specify
-a class and static method to get the connection from. The method must
-return a `com.mongodb.DB` or a `com.mongodb.MongoClient`. If the `DB` is
-not authenticated, you must also specify a `username` and `password`. If
-you use the factory method for providing a connection, you must not
-specify the `databaseName`, `server`, or `port` attributes.
-
-|factoryMethodName |Method |See the documentation for attribute
-`factoryClassName`.
-
-|databaseName |String |If you do not specify a `factoryClassName` and
-`factoryMethodName` for providing a MongoDB connection, you must specify
-a MongoDB database name using this attribute. You must also specify a
-`username` and `password`. You can optionally also specify a `server`
-(defaults to localhost), and a `port` (defaults to the default MongoDB
-port).
-
-|server |String |See the documentation for attribute `databaseName`.
-
-|port |int |See the documentation for attribute `databaseName`.
-
-|username |String |See the documentation for attributes `databaseName`
-and `factoryClassName`.
-
-|password |String |See the documentation for attributes `databaseName`
-and `factoryClassName`.
-
-|capped |boolean |Enable support for
-https://docs.mongodb.com/manual/core/capped-collections/[capped
-collections]
-
-|collectionSize |int |Specify the size in bytes of the capped collection
-to use if enabled. The minimum size is 4096 bytes, and larger sizes will
-be increased to the nearest integer multiple of 256. See the capped
-collection documentation linked above for more information.
-|=======================================================================
-
-This appender is link:messages.html#MapMessage[MapMessage]-aware.
-
-Here are a few sample configurations for the NoSQLAppender and MongoDB2
-provider:
-
-[source,xml]
-----
-<?xml version="1.0" encoding="UTF-8"?>
-<Configuration status="error">
-  <Appenders>
-    <NoSql name="databaseAppender">
-      <MongoDb2 databaseName="applicationDb" collectionName="applicationLog" server="mongo.example.org"
-               username="loggingUser" password="abc123" />
-    </NoSql>
-  </Appenders>
-  <Loggers>
-    <Root level="warn">
-      <AppenderRef ref="databaseAppender"/>
-    </Root>
-  </Loggers>
-</Configuration>
-----
-
-[source,xml]
-----
-<?xml version="1.0" encoding="UTF-8"?>
-<Configuration status="error">
-  <Appenders>
-    <NoSql name="databaseAppender">
-      <MongoDb2 collectionName="applicationLog" factoryClassName="org.example.db.ConnectionFactory"
-               factoryMethodName="getNewMongoClient" />
-    </NoSql>
-  </Appenders>
-  <Loggers>
-    <Root level="warn">
-      <AppenderRef ref="databaseAppender"/>
-    </Root>
-  </Loggers>
-</Configuration>
-----
-
-Starting in Log4j version 2.11.0, the provider element name is
-`MongoDb2`. The name `MongoDb` is now a deprecated alias for `MongoDb2`.
+We no longer provide the modules `log4j-mongodb` and `log4j-mongodb2`.
 
 [#NoSQLAppenderMongoDB3]
 == NoSQLAppenderMongoDB3
@@ -2156,6 +2046,74 @@
 </Configuration>
 ----
 
+[#NoSQLAppenderMongoDB4]
+== NoSQLAppenderMongoDB4
+
+This section details specializations of the
+link:#NoSQLAppender[NoSQLAppender] provider for MongoDB using the
+MongoDB driver version 4. The NoSQLAppender Appender writes log events
+to a NoSQL database using an internal lightweight provider interface.
+
+.MongoDB3 Provider Parameters
+[cols=",,",options="header",]
+|=======================================================================
+|Parameter Name |Type |Description
+|connection |String |_Required._ The MongoDB 
+http://mongodb.github.io/mongo-java-driver/4.0/apidocs/mongodb-driver-core/com/mongodb/ConnectionString.html?is-external=true"[connection string] 
+in the format `mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database.collection][?options]]`.
+
+|capped |boolean |Enable support for
+https://docs.mongodb.com/manual/core/capped-collections/[capped
+collections]
+
+|collectionSize |int |Specify the size in bytes of the capped collection
+to use if enabled. The minimum size is 4096 bytes, and larger sizes will
+be increased to the nearest integer multiple of 256. See the capped
+collection documentation linked above for more information.
+|=======================================================================
+
+This appender is link:messages.html#MapMessage[MapMessage]-aware.
+
+Here are a few sample configurations for the NoSQLAppender and MongoDB4
+provider:
+
+[source,xml]
+----
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+  <Appenders>
+    <NoSql name="MongoDbAppender">
+      <MongoDb4 connection="mongodb://log4jUser:12345678@localhost:${sys:MongoDBTestPort:-27017}/testDb.testCollection" />
+    </NoSql>
+  </Appenders>
+  <Loggers>
+    <Root level="ALL">
+      <AppenderRef ref="MongoDbAppender" />
+    </Root>
+  </Loggers>
+</Configuration>
+----
+
+[source,xml]
+----
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+  <Appenders>
+    <NoSql name="MongoDbAppender">
+      <MongoDb4 
+        connection="mongodb://localhost:${sys:MongoDBTestPort:-27017}/testDb.testCollection" 
+        capped="true" 
+        collectionSize="1073741824"/>
+    </NoSql>
+  </Appenders>
+  <Loggers>
+    <Root level="ALL">
+      <AppenderRef ref="MongoDbAppender" />
+    </Root>
+  </Loggers>
+</Configuration>
+----
+
 [#NoSQLAppenderCouchDB]
 == NoSQLAppenderCouchDB
 
diff --git a/src/site/asciidoc/manual/async.adoc b/src/site/asciidoc/manual/async.adoc
index 3b63d62..c89921b 100644
--- a/src/site/asciidoc/manual/async.adoc
+++ b/src/site/asciidoc/manual/async.adoc
@@ -209,7 +209,7 @@
 is determined by the
 link:../log4j-core/apidocs/org/apache/logging/log4j/core/async/AsyncQueueFullPolicy.html[AsyncQueueFullPolicy].
 
-|log4j2.asyncLoggerWaitStrategy
+|[[asyncLoggerWaitStrategy]]log4j2.asyncLoggerWaitStrategy
 |`Timeout`
 |Valid values: Block,
 Timeout, Sleep, Yield.
@@ -232,6 +232,23 @@
 performance and CPU resource, but may use more CPU than Sleep in order
 to get the message logged to disk sooner.
 
+|log4j2.asyncLoggerTimeout
+|`10`
+|Timeout in milliseconds of `TimeoutBlockingWaitStrategy`. See
+link:#asyncLoggerWaitStrategy[WaitStrategy System Property] for details.
+
+|log4j2.asyncLoggerSleepTimeNs
+|`100`
+|Sleep time (in nanoseconds) of `SleepingWaitStrategy`. See
+link:#asyncLoggerWaitStrategy[WaitStrategy System Property] for details.
+
+|log4j2.asyncLoggerRetries
+|`200`
+|Total number of spin cycles and `Thread.yield()` cycles of `SleepingWaitStrategy`. See
+link:#asyncLoggerWaitStrategy[WaitStrategy System Property] for details.
+
+
+
 |AsyncLogger.SynchronizeEnqueueWhenQueueFull
 |`true`
 |Synchronizes access to the Disruptor ring buffer for blocking enqueue operations when the queue is full.
@@ -375,7 +392,7 @@
 is determined by the
 link:../log4j-core/apidocs/org/apache/logging/log4j/core/async/AsyncQueueFullPolicy.html[AsyncQueueFullPolicy].
 
-|log4j2.asyncLoggerConfigWaitStrategy
+|[[asyncLoggerConfigWaitStrategy]]log4j2.asyncLoggerConfigWaitStrategy
 |`Timeout`
 |Valid values: Block,
 Timeout, Sleep, Yield. +
@@ -398,6 +415,21 @@
 performance and CPU resource, but may use more CPU than Sleep in order
 to get the message logged to disk sooner.
 
+|log4j2.asyncLoggerConfigTimeout
+|`10`
+|Timeout in milliseconds of `TimeoutBlockingWaitStrategy`. See
+link:#asyncLoggerConfigWaitStrategy[WaitStrategy System Property] for details.
+
+|log4j2.asyncLoggerConfigSleepTimeNs
+|`100`
+|Sleep time (in nanoseconds) of `SleepingWaitStrategy`. See
+link:#asyncLoggerConfigWaitStrategy[WaitStrategy System Property] for details.
+
+|log4j2.asyncLoggerConfigRetries
+|`200`
+|Total number of spin cycles and `Thread.yield()` cycles of `SleepingWaitStrategy`. See
+link:#asyncLoggerConfigWaitStrategy[WaitStrategy System Property] for details.
+
 |AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull
 |`true`
 |Synchronizes access to the Disruptor ring buffer for blocking enqueue operations when the queue is full.
diff --git a/src/site/asciidoc/manual/configuration.adoc b/src/site/asciidoc/manual/configuration.adoc
index 21bb180..1ff74b1 100644
--- a/src/site/asciidoc/manual/configuration.adoc
+++ b/src/site/asciidoc/manual/configuration.adoc
@@ -2056,6 +2056,30 @@
 link:async.html#SysPropsAllAsync[Async Logger System Properties] for
 details.
 
+|[[asyncLoggerTimeout]]log4j2.asyncLoggerTimeout +
+([[AsyncLogger.Timeout]]AsyncLogger.Timeout)
+|LOG4J_ASYNC_LOGGER_TIMEOUT
+|10
+|See
+link:async.html#SysPropsAllAsync[Async Logger System Properties] for
+details.
+
+|[[asyncLoggerSleepTimeNs]]log4j2.asyncLoggerSleepTimeNs +
+([[AsyncLogger.SleepTimeNs]]AsyncLogger.SleepTimeNs)
+|LOG4J_ASYNC_LOGGER_SLEEP_TIME_NS
+|100
+|See
+link:async.html#SysPropsAllAsync[Async Logger System Properties] for
+details.
+
+|[[asyncLoggerRetries]]log4j2.asyncLoggerRetries +
+([[AsyncLogger.Retries]]AsyncLogger.Retries)
+|LOG4J_ASYNC_LOGGER_RETRIES
+|200
+|See
+link:async.html#SysPropsAllAsync[Async Logger System Properties] for
+details.
+
 |[[AsyncLogger.SynchronizeEnqueueWhenQueueFull]]AsyncLogger.SynchronizeEnqueueWhenQueueFull
 |ASYNC_LOGGER_SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL
 |true
@@ -2095,6 +2119,31 @@
 link:async.html#SysPropsMixedSync-Async[Mixed Async/Synchronous Logger
 System Properties] for details.
 
+|[[asyncLoggerConfigTimeout]]log4j2.asyncLoggerConfigTimeout +
+([[AsyncLoggerConfig.Timeout]]AsyncLoggerConfig.Timeout)
+|LOG4J_ASYNC_LOGGER_CONFIG_TIMEOUT
+|10
+|See
+link:async.html#SysPropsMixedSync-Async[Mixed Async/Synchronous Logger
+System Properties] for details.
+
+|[[asyncLoggerConfigSleepTimeNs]]log4j2.asyncLoggerConfigSleepTimeNs +
+([[AsyncLoggerConfig.SleepTimeNs]]AsyncLoggerConfig.SleepTimeNs)
+|LOG4J_ASYNC_LOGGER_CONFIG_SLEEP_TIME_NS
+|100
+|See
+link:async.html#SysPropsMixedSync-Async[Mixed Async/Synchronous Logger
+System Properties] for details.
+
+|[[asyncLoggerConfigRetries]]log4j2.asyncLoggerConfigRetries +
+([[AsyncLoggerConfig.Retries]]AsyncLoggerConfig.Retries)
+|LOG4J_ASYNC_LOGGER_CONFIG_RETRIES
+|200
+|See
+link:async.html#SysPropsMixedSync-Async[Mixed Async/Synchronous Logger
+System Properties] for details.
+
+
 |[[AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull]]AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull
 |ASYNC_LOGGER_CONFIG_SYNCHRONIZE_ENQUEUE_WHEN_QUEUE_FULL
 |true
diff --git a/src/site/asciidoc/manual/extending.adoc b/src/site/asciidoc/manual/extending.adoc
index 6997928..f8ec37f 100644
--- a/src/site/asciidoc/manual/extending.adoc
+++ b/src/site/asciidoc/manual/extending.adoc
@@ -26,15 +26,15 @@
 
 The `LoggerContextFactory` binds the Log4j API to its implementation.
 The Log4j `LogManager` locates a `LoggerContextFactory` by using
-java.util.ServiceLoader to locate all instances of
+`java.util.ServiceLoader` to locate all instances of
 `org.apache.logging.log4j.spi.Provider`. Each implementation must
-provide a class that extends`org.apache.logging.log4j.spi.Provider` and
+provide a class that extends `org.apache.logging.log4j.spi.Provider` and
 should have a no-arg constructor that delegates to Provider's
 constructor passing the Priority, the API versions it is compatible
 with, and the class that implements
 `org.apache.logging.log4j.spi.LoggerContextFactory`. Log4j will compare
-the current API version and if it is compatible the implementation will
-be added to the list of providers. The API version in
+the current API version and if it is compatible the implementation 
+will be added to the list of providers. The API version in
 `org.apache.logging.log4j.LogManager` is only changed when a feature is
 added to the API that implementations need to be aware of. If more than
 one valid implementation is located the value for the Priority will be
@@ -47,15 +47,15 @@
 Applications may change the LoggerContextFactory that will be used by
 
 1.  Create a binding to the logging implementation.
-..  Implement a new `LoggerContextFactory`.
-..  Implement a class that extends `org.apache.logging.spi.Provider.`
+..  Implement a new link:../log4j-core/apidocs/org/apache/logging/log4j/core/impl/Log4jContextFactory.html[`LoggerContextFactory`].
+..  Implement a class that extends link:../log4j-core/apidocs/org/apache/logging/spi/Provider.html[`org.apache.logging.spi.Provider`] 
 with a no-arg constructor that calls super-class's constructor with the
 Priority, the API version(s), `LoggerContextFactory` class, and
-optionally, a `ThreadContextMap` implementation class.
+optionally, a link:../log4j-core/apidocs/org/apache/logging/log4j/spi/ThreadContextMap.html[`ThreadContextMap`] implementation class.
 ..  Create a `META-INF/services/org.apache.logging.spi.Provider` file
 that contains the name of the class that implements
 `org.apache.logging.spi.Provider`.
-2.  Setting the system property log4j2.loggerContextFactory to the name
+2.  Setting the system property "log4j2.loggerContextFactory" to the name
 of the `LoggerContextFactory` class to use.
 3.  Setting the property "log4j2.loggerContextFactory" in a properties
 file named "log4j2.LogManager.properties" to the name of the
@@ -82,7 +82,7 @@
   common LoggerContext.
 link:../log4j-core/apidocs/org/apache/logging/log4j/core/selector/ClassLoaderContextSelector.html[`ClassLoaderContextSelector`]::
   Associates LoggerContexts with the ClassLoader that created the caller
-  of the getLogger call. This is the default ContextSelector.
+  of the getLogger(...) call. This is the default ContextSelector.
 link:../log4j-core/apidocs/org/apache/logging/log4j/core/selector/JndiContextSelector.html[`JndiContextSelector`]::
   Locates the LoggerContext by querying JNDI.
 link:../log4j-core/apidocs/org/apache/logging/log4j/core/async/AsyncLoggerContextSelector.html[`AsyncLoggerContextSelector`]::
@@ -106,18 +106,16 @@
 The second method is by defining the `ConfigurationFactory` as a `Plugin`.
 
 All the ConfigurationFactories are then processed in order. Each factory
-is called on its `getSupportedTypes` method to determine the file
+is called on its `getSupportedTypes()` method to determine the file
 extensions it supports. If a configuration file is located with one of
 the specified file extensions then control is passed to that
-`ConfigurationFactory` to load the configuration and create the
-`Configuration` object.
+`ConfigurationFactory` to load the configuration and create the link:../log4j-core/apidocs/org/apache/logging/log4j/core/config/Configuration.html[`Configuration`] object.
 
-Most `Configuration` extend the `BaseConfiguration` class. This class
-expects that the subclass will process the configuration file and create
+Most `Configuration` extend the link:../log4j-core/apidocs/org/apache/logging/log4j/core/config/AbstractConfiguration.html[`AbstractConfiguration`] class. This class expects that the subclass will process the configuration file and create
 a hierarchy of `Node` objects. Each `Node` is fairly simple in that it
 consists of the name of the node, the name/value pairs associated with
 the node, The `PluginType` of the node and a List of all of its child
-Nodes. `BaseConfiguration` will then be passed the `Node` tree and
+Nodes. `Configuration` will then be passed the `Node` tree and
 instantiate the configuration objects from that.
 
 [source,java]
@@ -157,7 +155,7 @@
 
 `LoggerConfig` objects are where Loggers created by applications tie into
 the configuration. The Log4j implementation requires that all
-LoggerConfigs be based on the LoggerConfig class, so applications
+LoggerConfigs are based on the LoggerConfig class, so applications
 wishing to make changes must do so by extending the `LoggerConfig` class.
 To declare the new `LoggerConfig`, declare it as a Plugin of type "Core"
 and providing the name that applications should specify as the element
@@ -258,7 +256,7 @@
 [#Filters]
 == Filters
 
-As might be expected, Filters are the used to reject or accept log
+As might be expected, Filters are used to reject or accept log
 events as they pass through the logging system. A Filter is declared
 using a `@Plugin` annotation of `type` "Core" and an `elementType` of "filter".
 The `name` attribute on the Plugin annotation is used to specify the name
@@ -267,7 +265,7 @@
 `toString` will format the arguments to the filter as the configuration is
 being processed. The Filter must also specify a `@PluginFactory` method
 or `@PluginFactoryBuilder` builder class and method
-that will be called to create the Filter
+that will be called to create the Filter.
 
 The example below shows a Filter used to reject LogEvents based upon
 their logging level. Notice the typical pattern where all the filter
@@ -408,6 +406,8 @@
 
     protected SampleLayout(boolean locationInfo, boolean properties, boolean complete,
                            Charset charset) {
+        super(charset);
+        // handle the boolean parameters
     }
 
     @PluginFactory
@@ -455,9 +455,18 @@
     public static QueryConverter newInstance(final String[] options) {
       return new QueryConverter(options);
     }
+    
+    @Override
+    public void format(LogEvent event, StringBuilder toAppendTo) {
+        // get the data from 'event', to the work and append the result to 'toAppendTo'.
+    }    
 }
 ----
 
+A pattern to use this converter could be specified as `... %q ...` or `... %q{argument} ...`. 
+The "argument" will be passed as first (and only) value to the `options` parameter of the 
+`newInstance(...)` method.
+
 [#Plugin_Builders]
 == Plugin Builders
 
@@ -570,13 +579,15 @@
 [#Custom_ContextDataProvider]
 == Custom ContextDataProvider
 
-
-The `ContextDataProvider` (introduced in Log4j 2.13.2) is an interface applications and libraries can use to inject
-additional key-value pairs into the LogEvent's context data. Log4j's `ThreadContextDataInjector` uses
-`java.util.ServiceLoader` to locate and load `ContextDataProvider` instances. Log4j itself adds the ThreadContext data
-to the LogEvent using `org.apache.logging.log4j.core.impl.ThreadContextDataProvider`. Custom implementations
-should implement the `org.apache.logging.log4j.core.util.ContextDataProvider`interfaceand declare it as a service by
-defining the implmentation class in a file named
+The link:../log4j-core/apidocs/org/apache/logging/log4j/core/util/ContextDataProvider.html[`ContextDataProvider`] 
+(introduced in Log4j 2.13.2) is an interface applications and libraries can use to inject
+additional key-value pairs into the LogEvent's context data. Log4j's 
+link:../log4j-core/apidocs/org/apache/logging/log4j/core/impl/ThreadContextDataInjector.html[`ThreadContextDataInjector`] 
+uses `java.util.ServiceLoader` to locate and load `ContextDataProvider` instances.
+Log4j itself adds the ThreadContext data to the LogEvent using 
+`org.apache.logging.log4j.core.impl.ThreadContextDataProvider`. Custom implementations
+should implement the `org.apache.logging.log4j.core.util.ContextDataProvider` interface and 
+declare it as a service by defining the implmentation class in a file named
 `META-INF/services/org.apache.logging.log4j.core.util.ContextDataProvider`.
 
 == Custom ThreadContextMap implementations
@@ -585,10 +596,10 @@
 system property `log4j2.garbagefreeThreadContextMap` to true. (Log4j
 must be link:garbagefree.html#Config[enabled] to use ThreadLocals.)
 
-Any custom `ThreadContextMap` implementation can be installed by setting
-system property `log4j2.threadContextMap` to the fully qualified class
-name of the class implementing the `ThreadContextMap` interface. By also
-implementing the `ReadOnlyThreadContextMap` interface, your custom
+Any custom link:../log4j-core/apidocs/org/apache/logging/log4j/spi/ThreadContextMap.html[`ThreadContextMap`]
+implementation can be installed by setting system property `log4j2.threadContextMap` 
+to the fully qualified class name of the class implementing the `ThreadContextMap` 
+interface. By also implementing the `ReadOnlyThreadContextMap` interface, your custom
 `ThreadContextMap` implementation will be accessible to applications via the
 link:../log4j-api/apidocs/org/apache/logging/log4j/ThreadContext.html#getThreadContextMap()[`ThreadContext::getThreadContextMap`]
 method.
diff --git a/src/site/asciidoc/manual/garbagefree.adoc b/src/site/asciidoc/manual/garbagefree.adoc
index cd8d083..f18f6cb 100644
--- a/src/site/asciidoc/manual/garbagefree.adoc
+++ b/src/site/asciidoc/manual/garbagefree.adoc
@@ -179,6 +179,11 @@
 GelfLayout is garbage-free when used with compressionType="OFF", as long
 as no additional field contains '${' (variable substitution).
 
+==== JsonTemplateLayout
+
+`JsonTemplateLayout` is garbage-free with
+link:json-template-layout.html#faq-garbage-free[a few exceptions].
+
 ==== PatternLayout
 
 PatternLayout with the following limited set of conversion patterns is
diff --git a/src/site/asciidoc/manual/json-template-layout.adoc b/src/site/asciidoc/manual/json-template-layout.adoc
new file mode 100644
index 0000000..92ec635
--- /dev/null
+++ b/src/site/asciidoc/manual/json-template-layout.adoc
@@ -0,0 +1,1209 @@
+////
+    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.
+////
+= JSON Template Layout
+Volkan Yazıcı <vy@apache.org>
+
+`JsonTemplateLayout` is a customizable, efficient, and garbage-free JSON
+emitting layout. It encodes ``LogEvent``s according to the structure described
+by the JSON template provided. In a nutshell, it shines with its
+
+* Customizable JSON structure (see `eventTemplate[Uri]` and
+  `stackTraceElementTemplate[Uri]` parameters)
+
+* Customizable timestamp formatting (see `timestamp` parameter)
+
+[#usage]
+== Usage
+
+Adding `log4j-layout-json-template` artifact to your list of dependencies is
+enough to enable access to `JsonTemplateLayout` in your Log4j configuration:
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.logging.log4j</groupId>
+    <artifactId>log4j-layout-json-template</artifactId>
+    <version>${log4j.version}</version>
+</dependency>
+----
+
+For instance, given the following JSON template modelling the
+https://github.com/logstash/log4j-jsonevent-layout[the official Logstash
+`JSONEventLayoutV1`] (accessible via `classpath:LogstashJsonEventLayoutV1.json`)
+
+[source,json]
+----
+{
+  "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"
+  }
+}
+----
+
+in combination with the below `log4j2.xml` configuration:
+
+[source,xml]
+----
+<JsonTemplateLayout eventTemplateUri="classpath:LogstashJsonEventLayoutV1.json"/>
+----
+
+or with the below `log4j2.properties` configuration:
+
+[source,ini]
+----
+appender.console.json.type = JsonTemplateLayout
+appender.console.json.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.json
+----
+
+`JsonTemplateLayout` emits JSON strings as follows:
+
+[source,json]
+----
+{
+  "exception": {
+    "exception_class": "java.lang.RuntimeException",
+    "exception_message": "test",
+    "stacktrace": "java.lang.RuntimeException: test\n\tat org.apache.logging.log4j.JsonTemplateLayoutDemo.main(JsonTemplateLayoutDemo.java:11)\n"
+  },
+  "line_number": 12,
+  "class": "org.apache.logging.log4j.JsonTemplateLayoutDemo",
+  "@version": 1,
+  "source_host": "varlik",
+  "message": "Hello, error!",
+  "thread_name": "main",
+  "@timestamp": "2017-05-25T19:56:23.370+02:00",
+  "level": "ERROR",
+  "file": "JsonTemplateLayoutDemo.java",
+  "method": "main",
+  "logger_name": "org.apache.logging.log4j.JsonTemplateLayoutDemo"
+}
+----
+
+[#layout-config]
+== Layout Configuration
+
+`JsonTemplateLayout` is configured with the following parameters:
+
+.`JsonTemplateLayout` parameters
+[cols="1m,1m,4"]
+|===
+| Parameter Name
+| Type
+| Description
+
+| charset
+| Charset
+| `Charset` used for `String` encoding
+
+| locationInfoEnabled
+| boolean
+| toggles access to the `LogEvent` source; file name, line number, etc.
+  (defaults to `false` set by `log4j.layout.jsonTemplate.locationInfoEnabled`
+  property)
+
+| stackTraceEnabled
+| boolean
+| toggles access to the stack traces (defaults to `true` set by
+  `log4j.layout.jsonTemplate.stackTraceEnabled` property)
+
+| eventTemplate
+| String
+| inline JSON template for rendering ``LogEvent``s (has priority over
+  `eventTemplateUri`, defaults to `null` set by
+  `log4j.layout.jsonTemplate.eventTemplate` property)
+
+| eventTemplateUri
+| String
+| URI pointing to the JSON template for rendering ``LogEvent``s (defaults to
+  `classpath:EcsLayout.json` set by `log4j.layout.jsonTemplate.eventTemplateUri`
+  property)
+
+| eventTemplateAdditionalFields
+| EventTemplateAdditionalField[]
+| additional key-value pairs appended to the root of the event template
+
+| stackTraceElementTemplate
+| String
+| inline JSON template for rendering ``StackTraceElement``s (has priority over
+  `stackTraceElementTemplateUri`, defaults to `null` set by
+  `log4j.layout.jsonTemplate.stackTraceElementTemplate` property)
+
+| stackTraceElementTemplateUri
+| String
+| JSON template for rendering ``StackTraceElement``s (defaults to
+  `classpath:StackTraceElementLayout.json` set by
+  `log4j.layout.jsonTemplate.stackTraceElementTemplateUri` property)
+
+| eventDelimiter
+| String
+| delimiter used for separating emitted ``LogEvent``s (defaults to
+  `System.lineSeparator()` set by `log4j.layout.jsonTemplate.eventDelimiter`
+  property)
+
+| nullEventDelimiterEnabled
+| boolean
+| append `\0` (`null`) character to the end of every emitted `eventDelimiter`
+  (defaults to `false` set by
+  `log4j.layout.jsonTemplate.nullEventDelimiterEnabled` property)
+
+| maxStringLength
+| int
+| truncate string values longer than the specified limit (defaults to 16384 set
+  by `log4j.layout.jsonTemplate.maxStringLength` property)
+
+| truncatedStringSuffix
+| String
+| suffix to append to strings truncated due to exceeding `maxStringLength`
+  (defaults to `…` set by `log4j.layout.jsonTemplate.truncatedStringSuffix`
+  property)
+
+| recyclerFactory
+| RecyclerFactory
+| recycling strategy that can either be `dummy`, `threadLocal`, or `queue`
+  (set by `log4j.layout.jsonTemplate.recyclerFactory` property)
+|===
+
+[#additional-event-template-fields]
+=== Additonal event template fields
+
+Additional event template field is a convenient short-cut to add custom fields
+to a template or override the fields of a template. Following configuration
+overrides the `host` field of the `GelfLayout.json` template and adds two new
+custom fields:
+
+[source,xml]
+----
+<JsonTemplateLayout eventTemplateUri="classpath:GelfLayout.json">
+    <EventTemplateAdditionalFields>
+        <EventTemplateAdditionalField key="host" value="www.apache.org"/>
+        <EventTemplateAdditionalField key="_serviceName" value="auth-service"/>
+        <EventTemplateAdditionalField key="_containerId" value="6ede3f0ca7d9"/>
+    </EventTemplateAdditionalFields>
+</JsonTemplateLayout>
+----
+
+One can also pass JSON literals into additional fields:
+
+[source,xml]
+----
+<EventTemplateAdditionalField
+     key="marker"
+     type="JSON"
+     value='{"$resolver": "marker", "field": "name"}'/>
+<EventTemplateAdditionalField
+     key="aNumber"
+     type="JSON"
+     value="1"/>
+<EventTemplateAdditionalField
+     key="aList"
+     type="JSON"
+     value='[1,2,"string"]'/>
+----
+
+[#recycling-strategy]
+=== Recycling strategy
+
+`RecyclerFactory` plays a crucial role for determining the memory footprint of
+the layout. Template resolvers employ it to create recyclers for objects that
+they can reuse. The function of each `RecyclerFactory` and when one should
+prefer one over another is explained below:
+
+* `dummy` performs no recycling, hence each recycling attempt will result in a
+new instance. This will obviously create a load on the garbage-collector. It
+is a good choice for applications with low and medium log rate.
+
+* `threadLocal` performs the best, since every instance is stored in
+``ThreadLocal``s and accessed without any synchronization cost. Though this
+might not be a desirable option for applications running with hundreds of
+threads or more, e.g., a web servlet.
+
+* `queue` is the best of both worlds. It allows recycling of objects up to a
+certain number (`capacity`). When this limit is exceeded due to excessive
+concurrent load (e.g., `capacity` is 50 but there are 51 threads concurrently
+trying to log), it starts allocating. `queue` is a good strategy where
+`threadLocal` is not desirable.
++
+`queue` also accepts optional `supplier` (of type `java.util.Queue`, defaults to
+  `org.jctools.queues.MpmcArrayQueue.new` if JCTools is in the classpath;
+otherwise `java.util.concurrent.ArrayBlockingQueue.new`) and `capacity` (of
+type `int`, defaults to `max(8,2*cpuCount+1)`) parameters:
++
+[source]
+----
+queue:supplier=org.jctools.queues.MpmcArrayQueue.new
+queue:capacity=10
+queue:supplier=java.util.concurrent.ArrayBlockingQueue.new,capacity=50
+----
+
+The default `RecyclerFactory` is `threadLocal`, if
+`log4j2.enable.threadlocals=true`; otherwise, `queue`.
+
+[#template-config]
+== Template Configuration
+
+Templates are configured by means of the following `JsonTemplateLayout`
+parameters:
+
+- `eventTemplate[Uri]` (for serializing ``LogEvent``s)
+- `stackTraceElementTemplate[Uri]` (for serializing ``StackStraceElement``s)
+- `eventTemplateAdditionalFields` (for extending the used event template)
+
+[#event-templates]
+=== Event Templates
+
+`eventTemplate[Uri]` describes the JSON structure `JsonTemplateLayout` uses to
+serialize ``LogEvent``s. The default configuration (accessible by
+`log4j.layout.jsonTemplate.eventTemplate[Uri]` property) is set to
+`classpath:EcsLayout.json` provided by the `log4j-layout-json-template`
+artifact:
+
+[source,json]
+----
+{
+  "@timestamp": {
+    "$resolver": "timestamp",
+    "pattern": {
+      "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+      "timeZone": "UTC"
+    }
+  },
+  "log.level": {
+    "$resolver": "level",
+    "field": "name"
+  },
+  "message": {
+    "$resolver": "message",
+    "stringified": true
+  },
+  "process.thread.name": {
+    "$resolver": "thread",
+    "field": "name"
+  },
+  "log.logger": {
+    "$resolver": "logger",
+    "field": "name"
+  },
+  "labels": {
+    "$resolver": "mdc",
+    "flatten": true,
+    "stringified": true
+  },
+  "tags": {
+    "$resolver": "ndc"
+  },
+  "error.type": {
+    "$resolver": "exception",
+    "field": "className"
+  },
+  "error.message": {
+    "$resolver": "exception",
+    "field": "message"
+  },
+  "error.stack_trace": {
+    "$resolver": "exception",
+    "field": "stackTrace",
+    "stringified": true
+  }
+}
+
+----
+
+`log4j-layout-json-template` artifact contains the following predefined event
+templates:
+
+- https://github.com/apache/logging-log4j2/tree/master/log4j-layout-json-template/src/main/resources/EcsLayout.json[`EcsLayout.json`]
+  described by https://www.elastic.co/guide/en/ecs/current/ecs-reference.html[the Elastic Common Schema (ECS) specification]
+
+- https://github.com/apache/logging-log4j2/tree/master/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json[`LogstashJsonEventLayoutV1.json`]
+  described in https://github.com/logstash/log4j-jsonevent-layout[Logstash
+  `json_event` pattern for log4j]
+
+- https://github.com/apache/logging-log4j2/tree/master/log4j-layout-json-template/src/main/resources/GelfLayout.json[`GelfLayout.json`]
+  described by https://docs.graylog.org/en/3.1/pages/gelf.html#gelf-payload-specification[the
+  Graylog Extended Log Format (GELF) payload specification] with additional
+  `_thread` and `_logger` fields. (Here it is advised to override the obligatory
+  `host` field with a user provided constant via `eventTemplateAdditionalFields`
+  to avoid `hostName` property lookup at runtime, which incurs an extra cost.)
+
+- https://github.com/apache/logging-log4j2/tree/master/log4j-layout-json-template/src/main/resources/JsonLayout.json[`JsonLayout.json`]
+  providing the exact JSON structure generated by link:layouts.html#JSONLayout[`JsonLayout`]
+  with the exception of `thrown` field. (`JsonLayout` serializes the `Throwable`
+  as is via Jackson `ObjectMapper`, whereas `JsonLayout.json` template of
+  `JsonTemplateLayout` employs the `StackTraceElementLayout.json` template
+  for stack traces to generate a document-store-friendly flat structure.)
+
+Below is the list of supported event template resolvers:
+
+[#event-template-resolvers]
+.`LogEvent` template resolvers
+[cols="1m,3,2,2,4"]
+|===
+| Resolver Name
+| Syntax
+| Description
+| Garbage Footprint
+| Examples
+
+| endOfBatch
+|
+| `logEvent.isEndOfBatch()`
+| none
+a|
+[source,json]
+----
+{
+  "$resolver": "endOfBatch"
+}
+----
+
+| exception
+a|
+[source]
+----
+config      = field , [ stringified ]
+field       = "field" -> (
+                "className"  \|
+                "message"    \|
+                "stackTrace" )
+stringified = "stringified" -> boolean
+----
+a|
+Resolves fields of the `Throwable` returned by `logEvent.getThrown()`.
+
+Note that this resolver is toggled by
+`log4j.layout.jsonTemplate.stackTraceEnabled` property.
+| Since `Throwable#getStackTrace()` clones the original `StackTraceElement[]`,
+  access to (and hence rendering of) stack traces are not garbage-free.
+a|
+Resolve `logEvent.getThrown().getClass().getCanonicalName()`:
+
+[source,json]
+----
+{
+  "$resolver": "exception",
+  "field": "className"
+}
+----
+
+Resolve the stack trace into a list of `StackTraceElement` objects:
+
+[source,json]
+----
+{
+  "$resolver": "exception",
+  "field": "stackTrace"
+}
+----
+
+Resolve the stack trace into a string field:
+
+[source,json]
+----
+{
+  "$resolver": "exception",
+  "field": "stackTrace",
+  "stringified": true
+}
+----
+
+| exceptionRootCause
+| identical to `exception` resolver
+a|
+Resolves the fields of the innermost `Throwable` returned by
+`logEvent.getThrown()`.
+
+Note that this resolver is toggled by
+`log4j.layout.jsonTemplate.stackTraceEnabled` property.
+| identical to `exception` resolver
+| identical to `exception` resolver
+
+| level
+a|
+[source]
+----
+config         = field , [ severity ]
+field          = "field" -> ( "name" \| "severity" )
+severity       = severity-field
+severity-field = "field" -> ( "keyword" \| "code" )
+----
+| resolves the fields of the `logEvent.getLevel()`
+| none
+a|
+Resolve the level name:
+
+[source,json]
+----
+{
+  "$resolver": "level",
+  "field": "name"
+}
+----
+
+Resolve the https://en.wikipedia.org/wiki/Syslog#Severity_levels[Syslog severity]
+keyword:
+
+[source,json]
+----
+{
+  "$resolver": "level",
+  "field": "severity",
+  "severity": {
+    "field": "keyword"
+  }
+}
+----
+
+Resolve the https://en.wikipedia.org/wiki/Syslog#Severity_levels[Syslog severity]
+code:
+
+[source,json]
+----
+{
+  "$resolver": "level",
+  "field": "severity",
+  "severity": {
+    "field": "code"
+  }
+}
+----
+
+| logger
+a|
+[source]
+----
+config = "field" -> ( "name" \| "fqcn" )
+----
+| resolves `logEvent.getLoggerFqcn()` and `logEvent.getLoggerName()`
+| none
+a|
+Resolve the logger name:
+
+[source,json]
+----
+{
+  "$resolver": "logger",
+  "field": "name"
+}
+----
+
+Resolve the logger's fully qualified class name:
+
+[source,json]
+----
+{
+  "$resolver": "logger",
+  "field": "fqcn"
+}
+----
+
+| main:<key>
+a|
+[source]
+----
+config = ( index \| key )
+index  = "index" -> number
+key    = "key" -> string
+----
+| performs link:lookups.html#AppMainArgsLookup[Main Argument Lookup] for the
+  given `index` or `key`
+| none
+a|
+Resolve the 1st `main()` method argument:
+
+[source,json]
+----
+{
+  "$resolver": "main",
+  "index": 0
+}
+----
+
+Resolve the argument coming right after `--userId`:
+
+[source,json]
+----
+{
+  "$resolver": "main",
+  "key": "--userId"
+}
+----
+
+| map
+a|
+[source]
+----
+config      = key , [ stringified ]
+key         = "key" -> string
+stringified = "stringified" -> boolean
+----
+| resolves the given `key` of ``MapMessage``s
+| `stringified` flag translates to `String.valueOf(value)`, hence mind
+  not-`String`-typed values.
+a|
+Resolve the `userRole` field of the message:
+
+[source,json]
+----
+{
+  "$resolver": "map",
+  "key": "userRole"
+}
+----
+
+| marker
+a|
+[source]
+----
+config = "field" -> "name"
+----
+| `logEvent.getMarker().getName()`
+| none
+a|
+Resolve the marker name:
+
+[source,json]
+----
+{
+  "$resolver": "marker",
+  "field": "name"
+}
+----
+
+| mdc
+a|
+[source]
+----
+config        = singleAccess \| multiAccess
+
+singleAccess  = key , [ stringified ]
+key           = "key" -> string
+stringified   = "stringified" -> boolean
+
+multi-access  = [ pattern ] , [ flatten ] , [ stringified ]
+pattern       = "pattern" -> string
+flatten       = "flatten" -> ( boolean \| flattenConfig )
+flattenConfig = [ flattenPrefix ]
+flattenPrefix = "prefix" -> string
+----
+a| Mapped Diagnostic Context (MDC), aka. Thread Context Data, resolver.
+
+`singleAccess` resolves the MDC value as is, whilst `multiAccess` resolves a
+multitude of MDC values. If `flatten` is provided, `multiAccess` merges the
+values with the parent, otherwise creates a new JSON object containing the
+values.
+
+Enabling `stringified` flag converts each value to its string representation.
+
+Regex provided in the `pattern` is used to match against the keys.
+a|
+`log4j2.garbagefreeThreadContextMap` flag needs to be turned on to iterate
+the map without allocations.
+
+`stringified` allocates a new `String` for values that are not of type `String`.
+
+Writing certain non-primitive values (e.g., `BigDecimal`, `Set`, etc.) to JSON
+generates garbage, though most (e.g., `int`, `long`, `String`, `List`,
+`boolean[]`, etc.) don't.
+a|
+Resolve the `userRole` MDC value:
+
+[source,json]
+----
+{
+  "$resolver": "mdc",
+  "key": "userRole"
+}
+----
+
+Resolve the string representation of the `userRank` MDC value:
+
+[source,json]
+----
+{
+  "$resolver": "mdc",
+  "key": "userRank",
+  "stringified": true
+}
+----
+
+Resolve all MDC entries into an object:
+
+[source,json]
+----
+{
+  "$resolver": "mdc"
+}
+----
+
+Resolve all MDC entries into an object such that values are converted to string:
+
+[source,json]
+----
+{
+  "$resolver": "mdc",
+  "stringified": true
+}
+----
+
+Merge all MDC entries whose keys are matching with the `user(Role\|Rank)` regex
+into the parent:
+
+[source,json]
+----
+{
+  "$resolver": "mdc",
+  "flatten": true,
+  "pattern": "user(Role\|Rank)"
+}
+----
+
+After converting the corresponding entries to string, merge all MDC entries to
+parent such that keys are prefixed with `_`:
+
+[source,json]
+----
+{
+  "$resolver": "mdc",
+  "stringified": true,
+  "flatten": {
+    "prefix": "_"
+  }
+}
+----
+
+| message
+a|
+[source]
+----
+config      = [ stringified ] , [ fallbackKey ]
+pattern = "pattern" -> string
+includeStackTrace = "includeStacktrae" -> boolean
+stringified = "stringified" -> boolean
+fallbackKey = "fallbackKey" -> string
+----
+a| `logEvent.getMessage()`
+| For simple string messages, the resolution is performed without allocations.
+  For ``ObjectMessage``s and ``MultiformatMessage``s, it depends.
+a|
+Resolve the message into a string:
+
+[source,json]
+----
+{
+  "$resolver": "message",
+  "stringified": true
+}
+----
+
+Resolve the message into a string using a pattern:
+
+[source,json]
+----
+{
+  "$resolver": "message",
+  "pattern": ""[%t] %-5p %X{requestId, sessionId, loginId, userId, ipAddress, corpAcctNumber} %C{1.}.%M:%L - %m"",
+  "stringified": true
+}
+----
+
+Resolve the message such that if it is an `ObjectMessage` or a
+`MultiformatMessage` with JSON support, its type (string, list, object, etc.)
+will be retained:
+
+[source,json]
+----
+{
+  "$resolver": "message"
+}
+----
+
+Given the above configuration, a `SimpleMessage` will generate a `"sample log
+message"`, whereas a `MapMessage` will generate a `{"action": "login",
+"sessionId": "87asd97a"}`. Certain indexed log storage systems (e.g.,
+https://www.elastic.co/elasticsearch/[Elasticsearch]) will not allow both values
+to coexist due to type mismatch: one is a `string` while the other is an `object`.
+Here one can use a `fallbackKey` to work around the problem:
+
+[source,json]
+----
+{
+  "$resolver": "message",
+  "fallbackKey": "formattedMessage"
+}
+----
+
+Using this configuration, a `SimpleMessage` will generate a
+`{"formattedMessage": "sample log message"}` and a `MapMessage` will generate a
+`{"action": "login", "sessionId": "87asd97a"}`. Note that both emitted JSONs are
+of type `object` and have no type-conflicting fields.
+
+| ndc
+a|
+[source]
+----
+config  = [ pattern ]
+pattern = "pattern" -> string
+----
+| Resolves the Nested Diagnostic Context (NDC), aka. Thread Context Stack,
+  `String[]` returned by `logEvent.getContextStack()`
+| none
+a|
+Resolve all NDC values into a list:
+
+[source,json]
+----
+{
+  "$resolver": "ndc"
+}
+----
+
+Resolve all NDC values matching with the `pattern` regex:
+
+[source,json]
+----
+{
+  "$resolver": "ndc",
+  "pattern": "user(Role\|Rank):\\w+"
+}
+----
+
+| pattern
+a|
+[source]
+----
+config            = pattern , [ stackTraceEnabled ]
+pattern           = "pattern" -> string
+stackTraceEnabled = "stackTraceEnabled" -> boolean
+----
+a|
+Resolver delegating to link:layouts.html#PatternLayout[`PatternLayout`].
+
+The default value of `stackTraceEnabled` is inherited from the parent
+`JsonTemplateLayout`.
+| none
+a|
+Resolve the string produced by `%p %c{1.} [%t] %X{userId} %X %m%ex` pattern:
+
+[source,json]
+----
+{
+  "$resolver": "pattern",
+  "pattern": "%p %c{1.} [%t] %X{userId} %X %m%ex"
+}
+----
+
+| source
+a|
+[source]
+----
+config = "field" -> (
+           "className"  \|
+           "fileName"   \|
+           "methodName" \|
+           "lineNumber" )
+----
+a|
+Resolves the fields of the `StackTraceElement` returned by
+`logEvent.getSource()`.
+
+Note that this resolver is toggled by
+`log4j.layout.jsonTemplate.locationInfoEnabled` property.
+| none
+a|
+Resolve the line number:
+
+[source,json]
+----
+{
+  "$resolver": "source",
+  "field": "lineNumber"
+}
+----
+
+| thread
+a|
+[source]
+----
+config = "field" -> ( "name" \| "id" \| "priority" )
+----
+| resolves `logEvent.getThreadId()`, `logEvent.getThreadName()`,
+  `logEvent.getThreadPriority()`
+| none
+a|
+Resolve the thread name:
+
+[source,json]
+----
+{
+  "$resolver": "thread",
+  "field": "name"
+}
+----
+
+| timestamp
+a|
+[source]
+----
+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
+----
+| resolves `logEvent.getInstant()` in various forms
+| none
+a|
+.`timestamp` template resolver examples
+[cols="5,2m"]
+!===
+! Configuration
+! Output
+
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp"
+}
+----
+! 2020-02-07T13:38:47.098+02:00
+
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "pattern": {
+    "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+    "timeZone": "UTC",
+    "locale": "en_US"
+  }
+}
+----
+! 2020-02-07T13:38:47.098Z
+
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "epoch": {
+    "unit": "secs"
+  }
+}
+----
+! 1581082727.982123456
+
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "epoch": {
+    "unit": "secs",
+    "rounded": true
+  }
+}
+----
+! 1581082727
+
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "epoch": {
+    "unit": "secs.nanos"
+  }
+}
+----
+! 982123456
+
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "epoch": {
+    "unit": "millis"
+  }
+}
+----
+! 1581082727982.123456
+
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "epoch": {
+    "unit": "millis",
+    "rounded": true
+  }
+}
+----
+! 1581082727982
+
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "epoch": {
+    "unit": "millis.nanos"
+  }
+}
+----
+! 123456
+
+a!
+[source,json]
+----
+{
+  "$resolver": "timestamp",
+  "epoch": {
+    "unit": "nanos"
+  }
+}
+----
+! 1581082727982123456
+!===
+|===
+
+[#stack-trace-element-templates]
+=== Stack Trace Element Templates
+
+`stackTraceElement[Uri]` describes the JSON structure `JsonTemplateLayout` uses
+to format ``StackTraceElement``s. The default configuration (accessible by
+`log4j.layout.jsonTemplate.stackTraceElementTemplate[Uri]` property) is set to
+`classpath:StackTraceElementLayout.json` provided by the
+`log4j-layout-json-template` artifact:
+
+[source,json]
+----
+{
+  "class": {
+    "$resolver": "stackTraceElement",
+    "field": "className"
+  },
+  "method": {
+    "$resolver": "stackTraceElement",
+    "field": "methodName"
+  },
+  "file": {
+    "$resolver": "stackTraceElement",
+    "field": "fileName"
+  },
+  "line": {
+    "$resolver": "stackTraceElement",
+    "field": "lineNumber"
+  }
+}
+----
+
+The allowed template configuration syntax is as follows:
+
+[source]
+----
+config = "field" -> (
+           "className"  |
+           "fileName"   |
+           "methodName" |
+           "lineNumber" )
+----
+
+All above accesses to `StackTraceElement` is garbage-free.
+
+[#features]
+== Features
+
+Below is a feature comparison matrix between `JsonTemplateLayout` and
+alternatives.
+
+.Feature comparison matrix
+[cols="3,1,1,1,1"]
+|===
+| Feature
+| `JsonTemplateLayout`
+| link:layouts.html#JSONLayout[`JsonLayout`]
+| link:layouts.html#GELFLayout[`GelfLayout`]
+| https://github.com/elastic/java-ecs-logging/tree/master/log4j2-ecs-layout[`EcsLayout`]
+
+| Java version
+| 8
+| 8
+| 8
+| 6
+
+| Dependencies
+| None
+| Jackson
+| None
+| None
+
+| Full schema customization?
+| ✓
+| ✕
+| ✕
+| ✕
+
+| Timestamp customization?
+| ✓
+| ✕
+| ✕
+| ✕
+
+| (Almost) garbage-free?
+| ✓
+| ✕
+| ✓
+| ✓
+
+| Custom typed `Message` serialization?
+| ✓
+| ✕
+| ✕
+| ?footnote:[Only for ``ObjectMessage``s and if Jackson is in the classpath.]
+
+| Custom typed `MDC` value serialization?
+| ✓
+| ✕
+| ✕
+| ✕
+
+| Rendering stack traces as array?
+| ✓
+| ✓
+| ✕
+| ✓
+
+| JSON pretty print?
+| ✕
+| ✓
+| ✕
+| ✕
+
+| Additional fields?
+| ✓
+| ✓
+| ✓
+| ✓
+|===
+
+[#faq]
+== F.A.Q.
+
+[#faq-lookups]
+=== Are lookups supported in templates?
+
+Yes, link:lookups.html[lookups] (e.g., `${java:version}`, `${env:USER}`,
+`${date:MM-dd-yyyy}`) are supported in string literals of templates. Though note
+that they are not garbage-free.
+
+[#faq-garbage-free]
+=== Is `JsonTemplateLayout` garbage-free?
+
+Yes, if the garbage-free layout behaviour toggling properties
+`log4j2.enableDirectEncoders` and `log4j2.garbagefreeThreadContextMap` are
+enabled. Take into account the following caveats:
+
+* The configured link:#recycling-strategy[recycling strategy] might not be
+  garbage-free.
+
+* Since `Throwable#getStackTrace()` clones the original `StackTraceElement[]`,
+  access to (and hence rendering of) stack traces are not garbage-free.
+
+* Serialization of ``MapMessage``s and ``ObjectMessage``s are mostly
+  garbage-free except for certain types (e.g., `BigDecimal`, `BigInteger`,
+  ``Collection``s with the exception of `List`).
+
+* link:lookups.html[Lookups] (that is, `${...}` variables, excluding
+  `${json:...}` ones) are not garbage-free.
+
+Don't forget to checkout link:#event-template-resolvers[the notes on garbage footprint of resolvers]
+you employ in templates.
diff --git a/src/site/asciidoc/manual/layouts.adoc b/src/site/asciidoc/manual/layouts.adoc
index 3dbfa3a..a1d540f 100644
--- a/src/site/asciidoc/manual/layouts.adoc
+++ b/src/site/asciidoc/manual/layouts.adoc
@@ -15,7 +15,7 @@
     limitations under the License.
 ////
 = Layouts
-Ralph Goers <rgoers@apache.org>; Gary Gregory <ggregory@apache.org>
+Ralph Goers <rgoers@apache.org>; Gary Gregory <ggregory@apache.org>; Volkan Yazıcı <vy@apache.org>
 
 An Appender uses a Layout to format a LogEvent into a form that meets
 the needs of whatever will be consuming the log event. In Log4j 1.x and
@@ -490,6 +490,111 @@
 Additional link:../runtime-dependencies.html[runtime dependencies] are
 required for using JsonLayout.
 
+[#JSONTemplateLayout]
+== JSON Template Layout
+
+`JsonTemplateLayout` is a customizable, efficient, and garbage-free JSON
+emitting layout. It encodes ``LogEvent``s according to the structure described
+by the JSON template provided. For instance, given the following JSON template
+modelling the https://github.com/logstash/log4j-jsonevent-layout[the official
+Logstash `JSONEventLayoutV1`]
+
+[source,json]
+----
+{
+  "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"
+  }
+}
+----
+
+in combination with the below Log4j configuration:
+
+[source,xml]
+----
+<JsonTemplateLayout eventTemplateUri="classpath:LogstashJsonEventLayoutV1.json"/>
+----
+
+JSON Template Layout will render JSON documents as follows:
+
+[source,json]
+----
+{
+  "exception": {
+    "exception_class": "java.lang.RuntimeException",
+    "exception_message": "test",
+    "stacktrace": "java.lang.RuntimeException: test\n\tat org.apache.logging.log4j.JsonTemplateLayoutDemo.main(JsonTemplateLayoutDemo.java:11)\n"
+  },
+  "line_number": 12,
+  "class": "org.apache.logging.log4j.JsonTemplateLayoutDemo",
+  "@version": 1,
+  "source_host": "varlik",
+  "message": "Hello, error!",
+  "thread_name": "main",
+  "@timestamp": "2017-05-25T19:56:23.370+02:00",
+  "level": "ERROR",
+  "file": "JsonTemplateLayoutDemo.java",
+  "method": "main",
+  "logger_name": "org.apache.logging.log4j.JsonTemplateLayoutDemo"
+}
+----
+
+See link:json-template-layout.html[JSON Template Layout] page for the complete
+documentation.
+
 [#PatternLayout]
 == Pattern Layout
 
diff --git a/src/site/asciidoc/manual/lookups.adoc b/src/site/asciidoc/manual/lookups.adoc
index 06b6c6b..528644e 100644
--- a/src/site/asciidoc/manual/lookups.adoc
+++ b/src/site/asciidoc/manual/lookups.adoc
@@ -377,7 +377,7 @@
 ----
 <File name="Application" fileName="application.log">
   <PatternLayout>
-    <pattern>%d %p %c{1.} [%t] $${lower:{${spring:spring.application.name}} %m%n</pattern>
+    <pattern>%d %p %c{1.} [%t] $${lower:${spring:spring.application.name}} %m%n</pattern>
   </PatternLayout>
 </File>
 ----
diff --git a/src/site/asciidoc/manual/messages.adoc b/src/site/asciidoc/manual/messages.adoc
index 8c27dc3..cf0e069 100644
--- a/src/site/asciidoc/manual/messages.adoc
+++ b/src/site/asciidoc/manual/messages.adoc
@@ -228,8 +228,8 @@
 * When a link:appenders.html#JDBCAppender[JDBC Appender] is configured
 with a `MessageLayout`, it converts a Log4j `MapMessage` to values in a
 SQL INSERT statement.
-* When a link:appenders.html#NoSQLAppenderMongoDB2[MongoDB2 Appender] or
-link:appenders.html#NoSQLAppenderMongoDB3[MongoDB3 Appender] is
+* When a link:appenders.html#NoSQLAppenderMongoDB3[MongoDB3 Appender] or
+link:appenders.html#NoSQLAppenderMongoDB4[MongoDB4 Appender] is
 configured with a `MessageLayout`, it converts a Log4j `MapMessage` to
 fields in a MongoDB object.
 
diff --git a/src/site/markdown/manual/cloud.md b/src/site/markdown/manual/cloud.md
index 69020fd..84fb63f 100644
--- a/src/site/markdown/manual/cloud.md
+++ b/src/site/markdown/manual/cloud.md
@@ -48,7 +48,8 @@
 1. When performing audit logging using a framework such as log4j-audit guaranteed delivery of the audit events
 is required. Many of the options for writing the output, including writing to the standard output stream, do
 not guarantee delivery. In these cases the event must be delivered to a "forwarder" that acknowledges receipt
-only when it has placed the event in durable storage, such as what Apache Flume or Apache Kafka will do.
+only when it has placed the event in durable storage, such as what [Apache Flume](https://flume.apache.org/) 
+or [Apache Kafka](https://kafka.apache.org/) will do.
 
 ## Logging Approaches
 
@@ -58,7 +59,7 @@
 log analysis tools. 
 
 Note that any approach that bypasses Docker's logging drivers requires Log4j's 
-[Docker Loookup](lookups.html#DockerLookup) to allow Docker attributes to be injected into the log events.  
+[Docker Lookup](lookups.html#DockerLookup) to allow Docker attributes to be injected into the log events.  
 
 ### Logging to the Standard Output Stream
 
@@ -90,9 +91,9 @@
 ### Logging to a File
 
 While this is not the recommended 12-Factor approach, it performs very well. However, it requires that the 
-application declare a volume where the log files will reside and then configure the log forwarder to tail 
+application declares a volume where the log files will reside and then configures the log forwarder to tail 
 those files. Care must also be taken to automatically manage the disk space used for the logs, which Log4j 
-can perform via the Delete action on the [RollingFileAppender](appenders.html#RollingFileAppender).
+can perform via the "Delete" action on the [RollingFileAppender](appenders.html#RollingFileAppender).
 
 ![File](../images/DockerLogFile.png "Logging to a File")
 
@@ -118,13 +119,137 @@
 
 ![Aggregator](../images/LoggerAggregator.png "Application Logging to an Aggregator via TCP")
 
-## <a name="ELK"></a>Logging using ElasticSearch, Logstash, and Kibana
+## <a name="ELK"></a>Logging using Elasticsearch, Logstash, and Kibana
 
-The following configurations have been tested with an ELK stack and are known to work.
+There are various approaches with different trade-offs for ingesting logs into
+an ELK stack. Here we will briefly cover how one can forward Log4j generated
+events first to Logstash and then to Elasticsearch.
 
 ### Log4j Configuration
-Use a socket appender with the GELF layout. Note that if the host name used by the socket appender has more than 
-one ip address associated with its DNS entry the socket appender will fail through them all if needed.
+
+Log4j provides a multitude of JSON generating layouts. In particular, [JSON
+Template Layout](layouts.html#JSONTemplateLayout) allows full schema
+customization and bundles ELK-specific layouts by default, which makes it a
+great fit for the bill. Using the EcsLayout template as shown below will generate data in Kibana where
+the message displayed exactly matches the message passed to Log4j and most of the event attributes, including
+any exceptions, are present as individual attributes that can be displayed. Note, however that stack traces 
+will be formatted without newlines.
+
+    <Socket name="Logstash"
+            host="${sys:logstash.host}"
+            port="12345"
+            protocol="tcp"
+            bufferedIo="true">
+        <JsonTemplateLayout eventTemplateUri="classpath:EcsLayout.json">
+            <EventTemplateAdditionalFields>
+                <EventTemplateAdditionalField key="containerId" value="${docker:containerId:-}"/>
+                <EventTemplateAdditionalField key="application" value="${lower:${spring:spring.application.name:-spring}}"/>
+                <EventTemplateAdditionalField key="kubernetes.serviceAccountName" value="${k8s:accountName:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.containerId" value="${k8s:containerId:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.containerName" value="${k8s:containerName:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.host" value="${k8s:host:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.labels.app" value="${k8s:labels.app:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.labels.pod-template-hash" value="${k8s:labels.podTemplateHash:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.master_url" value="${k8s:masterUrl:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.namespaceId" value="${k8s:namespaceId:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.namespaceName" value="${k8s:namespaceName:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.podID" value="${k8s:podId:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.podIP" value="${k8s:podIp:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.podName" value="${k8s:podName:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.imageId" value="${k8s:imageId:-}"/>
+                <EventTemplateAdditionalField key="kubernetes.imageName" value="${k8s:imageName:-}"/>
+            </EventTemplateAdditionalFields>
+        </JsonTemplateLayout>
+    </Socket>
+    
+The JsonTemplateLayout can also be used to generate JSON that matches the GELF specification which can     
+format the message attribute using a pattern in accordance with the PatternLayout. For example, the following
+template, named EnhancedGelf.json, can be used to generate GELF-compliant data that can be passed to Logstash. 
+With this template the message attribute will include the thread id, level, specific ThreadContext attributes, 
+the class name, method name, and line number as well as the message. If an exception is included it will also 
+be included with newlines. This format follows very closely what you would see in a typical log file on disk 
+using the PatternLayout but has the additional advantage of including the attributes as separate fields that 
+can be queried.
+
+    {
+        "version": "1.1",
+        "host": "${hostName}",
+        "short_message": {
+            "$resolver": "message",
+            "stringified": true
+        },
+        "full_message": {
+            "$resolver": "message",
+            "pattern": "[%t] %-5p %X{requestId, sessionId, loginId, userId, ipAddress, corpAcctNumber} %C{1.}.%M:%L - %m",
+            "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
+        }
+    }
+    
+The logging configuration to use this template would be    
+
+    <Socket name="Elastic"
+            host="\${sys:logstash.search.host}"
+            port="12222"
+            protocol="tcp"
+            bufferedIo="true">
+      <JsonTemplateLayout eventTemplateUri="classpath:EnhancedGelf.json" nullEventDelimiterEnabled="true">
+        <EventTemplateAdditionalFields>
+          <EventTemplateAdditionalField key="containerId" value="${docker:containerId:-}"/>
+          <EventTemplateAdditionalField key="application" value="${lower:${spring:spring.application.name:-spring}}"/>
+          <EventTemplateAdditionalField key="kubernetes.serviceAccountName" value="${k8s:accountName:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.containerId" value="${k8s:containerId:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.containerName" value="${k8s:containerName:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.host" value="${k8s:host:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.labels.app" value="${k8s:labels.app:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.labels.pod-template-hash" value="${k8s:labels.podTemplateHash:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.master_url" value="${k8s:masterUrl:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.namespaceId" value="${k8s:namespaceId:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.namespaceName" value="${k8s:namespaceName:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.podID" value="${k8s:podId:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.podIP" value="${k8s:podIp:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.podName" value="${k8s:podName:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.imageId" value="${k8s:imageId:-}"/>
+          <EventTemplateAdditionalField key="kubernetes.imageName" value="${k8s:imageName:-}"/>
+        </EventTemplateAdditionalFields>
+      </JsonTemplateLayout>
+    </Socket>
+The significant difference with this configuration from the first example is that it references the 
+custom template and it specifies an event delimiter of a null character ('\0');   
+    
+Note: The level being passed with the above template does not strictly conform to the GELF spec as the
+Level being passed is the Log4j Level NOT the Level defined in the GELF spec. However, testing has shown 
+that Logstash, Elk, and Kibana are pretty tolerant of whatever data is passed to it.    
+    
+Finally, the GelfLayout can be used to generate GELF compliant output. Unlike the JsonTemplateLayout it 
+adheres closely to the GELF spec.    
 
     <Socket name="Elastic" host="${sys:elastic.search.host}" port="12222" protocol="tcp" bufferedIo="true">
       <GelfLayout includeStackTrace="true" host="${hostName}" includeThreadContext="true" includeNullDelimiter="true"
@@ -132,7 +257,7 @@
         <ThreadContextIncludes>requestId,sessionId,loginId,userId,ipAddress,callingHost</ThreadContextIncludes>
         <MessagePattern>%d [%t] %-5p %X{requestId, sessionId, loginId, userId, ipAddress} %C{1.}.%M:%L - %m%n</MessagePattern>
         <KeyValuePair key="containerId" value="${docker:containerId:-}"/>
-        <KeyValuePair key="application" value="$${lower:${spring:spring.application.name:-spring}}"/>
+        <KeyValuePair key="application" value="${lower:${spring:spring.application.name:-spring}}"/>
         <KeyValuePair key="kubernetes.serviceAccountName" value="${k8s:accountName:-}"/>
         <KeyValuePair key="kubernetes.containerId" value="${k8s:containerId:-}"/>
         <KeyValuePair key="kubernetes.containerName" value="${k8s:containerName:-}"/>
@@ -152,48 +277,74 @@
 
 ### Logstash Configuration
 
-    input {
-      gelf {
-        host => "localhost"
-        use_tcp => true
-        use_udp => false
-        port => 12222
-        type => "gelf"
-      }
-    }
+We will configure Logstash to listen on TCP port 12345 for payloads of type JSON
+and then forward these to (either console and/or) an Elasticsearch server.
 
-    filter {
-      # 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"
-        }
+    input {
+      tcp {
+        port => 12345
+        codec => "json"
       }
     }
 
     output {
-      # (Un)comment for debugging purposes
+
+      # (Un)comment for debugging purposes.
       # stdout { codec => rubydebug }
+
       # Modify the hosts value to reflect where elasticsearch is installed.
       elasticsearch {
         hosts => ["http://localhost:9200/"]
         index => "app-%{application}-%{+YYYY.MM.dd}"
       }
+
     }
+    
+When one of the GELF compliant formats is used Logstash should be configured as 
+
+   gelf {
+           host => "localhost"
+           use_tcp => true
+           use_udp => false
+           port => 12222
+           type => "gelf"
+         }
+       }
+   
+       filter {
+         # 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 }
+         # Modify the hosts value to reflect where elasticsearch is installed.
+         elasticsearch {
+           hosts => ["http://localhost:9200/"]
+           index => "app-%{application}-%{+YYYY.MM.dd}"
+         }
+       }
 
 ### Kibana
-With the above configurations the message field will contain a fully formatted log event just as it would  appear in 
-a file Appender. The ThreadContext attributes, custome fields, thread name, etc. will all be available as attributes
-on each log event that can be used for filtering.
+Using the EnhancedGelf template or the GelfLayout the above configurations the message field will contain a fully 
+formatted log event just as it would  appear in a file Appender. The ThreadContext attributes, custome fields, 
+thread name, etc. will all be available as attributes on each log event that can be used for filtering.
+The result will resemble
+![](../images/kibana.png)
 
 ## Managing Logging Configuration
 
@@ -250,7 +401,7 @@
 
 ## Appender Performance
 The numbers in the table below represent how much time in seconds was required for the application to 
-call logger.debug 100,000 times. These numbers only include the time taken to deliver to the specifically 
+call `logger.debug(...)` 100,000 times. These numbers only include the time taken to deliver to the specifically 
 noted endpoint and many not include the actual time required before they are available for viewing. All 
 measurements were performed on a MacBook Pro with a 2.9GHz Intel Core I9 processor with 6 physical and 12 
 logical cores, 32GB of 2400 MHz DDR4 RAM, and 1TB of Apple SSD storage. The VM used by Docker was managed 
@@ -322,4 +473,4 @@
 1. Logging to files within the container is discouraged. Doing so requires that a volume be declared in 
 the Docker configuration and that the file be tailed by a log forwarder. However, it performs 
 better than logging to the standard output stream. If logging via TCP is not an option and
-proper multiline handling is required then consider this option.
\ No newline at end of file
+proper multiline handling is required then consider this option.
diff --git a/src/site/resources/images/kibana.png b/src/site/resources/images/kibana.png
new file mode 100644
index 0000000..3b15693
--- /dev/null
+++ b/src/site/resources/images/kibana.png
Binary files differ
diff --git a/src/site/site.xml b/src/site/site.xml
index 4fa5a95..49437e8 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -176,8 +176,8 @@
         <item name="Memory Mapped File" href="/manual/appenders.html#MemoryMappedFileAppender"/>
         <item name="NoSQL" href="/manual/appenders.html#NoSQLAppender"/>
         <item name="NoSQL for MongoDB" href="/manual/appenders.html#NoSQLAppenderMongoDB"/>
-        <item name="NoSQL for MongoDB 2" href="/manual/appenders.html#NoSQLAppenderMongoDB2"/>
         <item name="NoSQL for MongoDB 3" href="/manual/appenders.html#NoSQLAppenderMongoDB3"/>
+        <item name="NoSQL for MongoDB 4" href="/manual/appenders.html#NoSQLAppenderMongoDB4"/>
         <item name="NoSQL for CouchDB" href="/manual/appenders.html#NoSQLAppenderCouchDB"/>
         <item name="Output Stream" href="/manual/appenders.html#OutputStreamAppender"/>
         <item name="Random Access File" href="/manual/appenders.html#RandomAccessFileAppender"/>
@@ -198,6 +198,7 @@
         <item name="GELF" href="/manual/layouts.html#GELFLayout"/>
         <item name="HTML" href="/manual/layouts.html#HTMLLayout"/>
         <item name="JSON" href="/manual/layouts.html#JSONLayout"/>
+        <item name="JSON Template" href="/manual/json-template-layout.html"/>
         <item name="Pattern" href="/manual/layouts.html#PatternLayout"/>
         <item name="RFC-5424" href="/manual/layouts.html#RFC5424Layout"/>
         <item name="Serialized" href="/manual/layouts.html#SerializedLayout"/>
@@ -316,8 +317,8 @@
       <item name="Log4j Web Application Support" href="log4j-web/index.html"/>
       <item name="Log4j Application Server Integration" href="log4j-appserver/index.html"/>
       <item name="Log4j CouchDB appender" href="log4j-couchdb/index.html"/>
-      <item name="Log4j MongoDB2 appender" href="log4j-mongodb2/index.html"/>
       <item name="Log4j MongoDB3 appender" href="log4j-mongodb3/index.html"/>
+      <item name="Log4j MongoDB4 appender" href="log4j-mongodb4/index.html"/>
       <item name="Log4j Cassandra appender" href="log4j-cassandra/index.html"/>
       <item name="Log4j IO Streams" href="log4j-iostreams/index.html"/>
       <item name="Log4j Liquibase Binding" href="log4j-liquibase/index.html"/>
diff --git a/toolchains-docker.xml b/toolchains-docker.xml
index 844e3ec..3f924a2 100644
--- a/toolchains-docker.xml
+++ b/toolchains-docker.xml
@@ -16,27 +16,24 @@
   ~ limitations under the license.
   -->
 <toolchains>
-  <!-- JDK toolchains -->
   <toolchain>
     <type>jdk</type>
     <provides>
-      <version>1.7</version>
-      <vendor>sun</vendor>
+      <version>1.8</version>
+      <vendor>openjdk</vendor>
     </provides>
     <configuration>
-      <jdkHome>/docker-java-home</jdkHome>
+      <jdkHome>/usr/local/openjdk-8</jdkHome>
     </configuration>
   </toolchain>
   <toolchain>
     <type>jdk</type>
     <provides>
-      <version>9</version>
-      <vendor>sun</vendor>
+      <version>11</version>
+      <vendor>openjdk</vendor>
     </provides>
     <configuration>
-      <jdkHome>/docker-java-9-home</jdkHome>
+      <jdkHome>/usr/local/openjdk-11</jdkHome>
     </configuration>
   </toolchain>
-
-  <!-- other toolchains -->
-</toolchains>
\ No newline at end of file
+</toolchains>
diff --git a/toolchains-jenkins-ubuntu.xml b/toolchains-jenkins-ubuntu.xml
deleted file mode 100644
index 09898d7..0000000
--- a/toolchains-jenkins-ubuntu.xml
+++ /dev/null
@@ -1,101 +0,0 @@
-<?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>
-  <!-- JDK toolchains -->
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>1.8</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>/home/jenkins/tools/java/latest1.8</jdkHome>
-    </configuration>
-  </toolchain>
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>9</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>/home/jenkins/tools/java/latest1.9</jdkHome>
-    </configuration>
-  </toolchain>
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>10</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>/home/jenkins/tools/java/latest10</jdkHome>
-    </configuration>
-  </toolchain>
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>11</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>/home/jenkins/tools/java/latest11/</jdkHome>
-    </configuration>
-  </toolchain>
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>12</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>/home/jenkins/tools/java/latest12/</jdkHome>
-    </configuration>
-  </toolchain>
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>13</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>/home/jenkins/tools/java/latest13/</jdkHome>
-    </configuration>
-  </toolchain>
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>14</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>/home/jenkins/tools/java/latest14/</jdkHome>
-    </configuration>
-  </toolchain>
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>15</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>/home/jenkins/tools/java/latest15/</jdkHome>
-    </configuration>
-  </toolchain>
-  <!-- other toolchains -->
-</toolchains>
diff --git a/toolchains-jenkins-win.xml b/toolchains-jenkins-win.xml
deleted file mode 100644
index df01b39..0000000
--- a/toolchains-jenkins-win.xml
+++ /dev/null
@@ -1,101 +0,0 @@
-<?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
-  ~
-  ~     https://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>
-  <!-- JDK toolchains -->
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>1.8</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>F:\jenkins\tools\java\latest1.8</jdkHome>
-    </configuration>
-  </toolchain>
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>9</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>F:\jenkins\tools\java\latest9</jdkHome>
-    </configuration>
-  </toolchain>
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>10</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>F:\jenkins\tools\java\latest10</jdkHome>
-    </configuration>
-  </toolchain>
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>11</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>F:\jenkins\tools\java\latest11</jdkHome>
-    </configuration>
-  </toolchain>
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>12</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>F:\jenkins\tools\java\latest12</jdkHome>
-    </configuration>
-  </toolchain>
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>13</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>F:\jenkins\tools\java\latest13</jdkHome>
-    </configuration>
-  </toolchain>
-  <toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>14</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>F:\jenkins\tools\java\latest14</jdkHome>
-    </configuration>
-  </toolchain>
-  <!--toolchain>
-    <type>jdk</type>
-    <provides>
-      <version>15</version>
-      <vendor>sun</vendor>
-    </provides>
-    <configuration>
-      <jdkHome>F:\jenkins\tools\java\latest15</jdkHome>
-    </configuration>
-  </toolchain-->
-  <!-- other toolchains -->
-</toolchains>