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