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/05/25 20:42:57 UTC
[logging-log4j2] branch master updated: #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 master
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/master by this push:
new a529d8d #335 Initial import of JsonTemplateLayout from LogstashLayout.
a529d8d is described below
commit a529d8dcf6397c9faafb54b2f65ee8694b7454f8
Author: Volkan Yazıcı <vo...@gmail.com>
AuthorDate: Mon May 25 22:42:48 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-layout-json-template/pom.xml | 552 +++
.../layout/json/template/JsonTemplateLayout.java | 543 +++
.../json/template/JsonTemplateLayoutDefaults.java | 204 ++
.../template/resolver/ContextDataResolver.java | 303 ++
.../resolver/ContextDataResolverFactory.java | 39 +
.../template/resolver/ContextStackResolver.java | 102 +
.../resolver/ContextStackResolverFactory.java | 39 +
.../json/template/resolver/EndOfBatchResolver.java | 44 +
.../resolver/EndOfBatchResolverFactory.java | 39 +
.../json/template/resolver/EventResolver.java | 21 +
.../template/resolver/EventResolverContext.java | 226 ++
.../template/resolver/EventResolverFactories.java | 65 +
.../template/resolver/EventResolverFactory.java | 21 +
.../resolver/ExceptionInternalResolverFactory.java | 71 +
.../json/template/resolver/ExceptionResolver.java | 111 +
.../resolver/ExceptionResolverFactory.java | 39 +
.../resolver/ExceptionRootCauseResolver.java | 116 +
.../ExceptionRootCauseResolverFactory.java | 39 +
.../json/template/resolver/LevelResolver.java | 115 +
.../template/resolver/LevelResolverFactory.java | 39 +
.../json/template/resolver/LoggerResolver.java | 61 +
.../template/resolver/LoggerResolverFactory.java | 39 +
.../json/template/resolver/MainMapResolver.java | 45 +
.../template/resolver/MainMapResolverFactory.java | 39 +
.../layout/json/template/resolver/MapResolver.java | 62 +
.../json/template/resolver/MapResolverFactory.java | 39 +
.../json/template/resolver/MarkerResolver.java | 64 +
.../template/resolver/MarkerResolverFactory.java | 39 +
.../json/template/resolver/MessageResolver.java | 171 +
.../template/resolver/MessageResolverFactory.java | 39 +
.../json/template/resolver/PatternResolver.java | 50 +
.../template/resolver/PatternResolverFactory.java | 39 +
.../json/template/resolver/SourceResolver.java | 117 +
.../template/resolver/SourceResolverFactory.java | 39 +
.../resolver/StackTraceElementObjectResolver.java | 66 +
.../StackTraceElementObjectResolverContext.java | 93 +
.../StackTraceElementObjectResolverFactories.java | 39 +
.../StackTraceElementObjectResolverFactory.java | 43 +
.../resolver/StackTraceObjectResolver.java | 54 +
.../json/template/resolver/StackTraceResolver.java | 19 +
.../template/resolver/StackTraceTextResolver.java | 51 +
.../json/template/resolver/TemplateResolver.java | 42 +
.../template/resolver/TemplateResolverContext.java | 34 +
.../template/resolver/TemplateResolverFactory.java | 25 +
.../json/template/resolver/TemplateResolvers.java | 355 ++
.../json/template/resolver/ThreadResolver.java | 68 +
.../template/resolver/ThreadResolverFactory.java | 39 +
.../json/template/resolver/TimestampResolver.java | 446 +++
.../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 +++++
.../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 | 126 +
.../src/main/resources/EcsLayout.json | 12 +
.../src/main/resources/GelfLayout.json | 11 +
.../src/main/resources/JsonLayout.json | 26 +
.../main/resources/LogstashJsonEventLayoutV1.json | 19 +
.../main/resources/StackTraceElementLayout.json | 6 +
.../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 | 80 +
.../log4j/layout/json/template/GelfLayoutTest.java | 102 +
.../log4j/layout/json/template/JacksonFixture.java | 36 +
.../log4j/layout/json/template/JsonLayoutTest.java | 102 +
.../JsonTemplateLayoutConcurrentEncodeTest.java | 192 ++
.../template/JsonTemplateLayoutGcFreeTest.java | 40 +
.../json/template/JsonTemplateLayoutTest.java | 1653 +++++++++
.../json/template/LayoutComparisonHelpers.java | 19 +
.../layout/json/template/LogEventFixture.java | 151 +
.../log4j/layout/json/template/LogstashIT.java | 485 +++
.../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 | 64 +
.../resources/gcFreeJsonTemplateLayoutLogging.xml | 22 +
.../src/test/resources/testJsonTemplateLayout.json | 21 +
log4j-perf/pom.xml | 20 +
.../json/template/JsonTemplateLayoutBenchmark.java | 185 ++
.../JsonTemplateLayoutBenchmarkReport.java | 359 ++
.../template/JsonTemplateLayoutBenchmarkState.java | 201 ++
.../log4j/perf/jmh/ThreadLocalVsPoolBenchmark.java | 252 +-
pom.xml | 3498 ++++++++++----------
src/site/asciidoc/manual/garbagefree.adoc | 5 +
src/site/asciidoc/manual/json-template-layout.adoc | 659 ++++
src/site/asciidoc/manual/layouts.adoc | 68 +-
src/site/markdown/manual/cloud.md | 97 +-
src/site/site.xml | 1 +
107 files changed, 16135 insertions(+), 1959 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 ea06dff..be50ab8 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/Strings.java
@@ -55,14 +55,23 @@ 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;
}
/**
@@ -286,4 +295,23 @@ public final class Strings {
return buf.toString();
}
+ /**
+ * Creates a new string repeating given {@code str} {@code count} times.
+ * @param str input string
+ * @param count the repetition count
+ * @return the new string
+ * @throws IllegalArgumentException if either {@code str} is null or {@code count} is negative
+ */
+ public static String repeat(final String str, final int count) {
+ Objects.requireNonNull(str, "str");
+ if (count < 0) {
+ throw new IllegalArgumentException("count");
+ }
+ StringBuilder sb = new StringBuilder(str.length() * count);
+ for (int index = 0; index < count; index++) {
+ sb.append(str);
+ }
+ return sb.toString();
+ }
+
}
diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/util/StringsTest.java b/log4j-api/src/test/java/org/apache/logging/log4j/util/StringsTest.java
index 162d162..e91e298 100644
--- a/log4j-api/src/test/java/org/apache/logging/log4j/util/StringsTest.java
+++ b/log4j-api/src/test/java/org/apache/logging/log4j/util/StringsTest.java
@@ -25,6 +25,25 @@ import java.util.Iterator;
public class StringsTest {
+ @Test
+ public void testIsEmpty() {
+ Assert.assertTrue(Strings.isEmpty(null));
+ Assert.assertTrue(Strings.isEmpty(""));
+ Assert.assertFalse(Strings.isEmpty(" "));
+ Assert.assertFalse(Strings.isEmpty("a"));
+ }
+
+ @Test
+ public void testIsBlank() {
+ Assert.assertTrue(Strings.isBlank(null));
+ Assert.assertTrue(Strings.isBlank(""));
+ Assert.assertTrue(Strings.isBlank(" "));
+ Assert.assertTrue(Strings.isBlank("\n"));
+ Assert.assertTrue(Strings.isBlank("\r"));
+ Assert.assertTrue(Strings.isBlank("\t"));
+ Assert.assertFalse(Strings.isEmpty("a"));
+ }
+
/**
* A sanity test to make sure a typo does not mess up {@link Strings#EMPTY}.
*/
diff --git a/log4j-bom/pom.xml b/log4j-bom/pom.xml
index ea845e5..0f21c78 100644
--- a/log4j-bom/pom.xml
+++ b/log4j-bom/pom.xml
@@ -72,6 +72,12 @@
<artifactId>log4j-layout-jackson-yaml</artifactId>
<version>${project.version}</version>
</dependency>
+ <!-- JSON template layout -->
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-layout-json-template</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<!-- Legacy Log4j 1.2 API -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/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 46efe64..938234f 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/GcFreeLoggingTestUtil.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/GcFreeLoggingTestUtil.java
@@ -16,35 +16,33 @@
*/
package org.apache.logging.log4j.core;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.io.File;
-import java.net.URL;
-import java.nio.charset.Charset;
-import java.nio.file.Files;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-
+import com.google.monitoring.runtime.instrumentation.AllocationRecorder;
+import com.google.monitoring.runtime.instrumentation.Sampler;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.core.util.Constants;
import org.apache.logging.log4j.message.StringMapMessage;
-import org.apache.logging.log4j.util.Strings;
-import com.google.monitoring.runtime.instrumentation.AllocationRecorder;
-import com.google.monitoring.runtime.instrumentation.Sampler;
+import java.io.File;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
/**
- * Utily methods for the GC-free logging tests.s.
+ * Utility methods for the GC-free logging tests.
*/
-public class GcFreeLoggingTestUtil {
+public enum GcFreeLoggingTestUtil {;
public static void executeLogging(final String configurationFile,
- final Class<?> testClass) throws Exception {
+ final Class<?> testClass) throws Exception {
System.setProperty("log4j2.enable.threadlocals", "true");
System.setProperty("log4j2.enable.direct.encoders", "true");
@@ -148,16 +146,36 @@ 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() throws Exception {
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-layout-json-template/pom.xml b/log4j-layout-json-template/pom.xml
new file mode 100644
index 0000000..f9e74d6
--- /dev/null
+++ b/log4j-layout-json-template/pom.xml
@@ -0,0 +1,552 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to You under the Apache license, Version 2.0
+ ~ (the "License"); you may not use this file except in compliance with
+ ~ the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the license for the specific language governing permissions and
+ ~ limitations under the license.
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j</artifactId>
+ <version>3.0.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>log4j-layout-json-template</artifactId>
+ <name>Apache Log4j Layout for JSON template</name>
+ <description>
+ Apache Log4j Layout for JSON template.
+ </description>
+
+ <properties>
+ <log4jParentDir>${basedir}/..</log4jParentDir>
+ <docLabel>Log4j Layout for JSON Template Documentation</docLabel>
+ <projectDir>/log4j-layout-json-template</projectDir>
+ <module.name>org.apache.logging.log4j.layout.json.template</module.name>
+ </properties>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-core</artifactId>
+ <version>${project.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-layout-jackson-json</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.jctools</groupId>
+ <artifactId>jctools-core</artifactId>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>co.elastic.logging</groupId>
+ <artifactId>log4j2-ecs-layout</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.code.java-allocation-instrumenter</groupId>
+ <artifactId>java-allocation-instrumenter</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.elasticsearch.client</groupId>
+ <artifactId>elasticsearch-rest-high-level-client</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ </dependencies>
+
+ <build>
+ <plugins>
+
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <configuration>
+ <instructions>
+ <Fragment-Host>org.apache.logging.log4j.layout.json.template</Fragment-Host>
+ <Export-Package>*</Export-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>default-jar</id>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ <configuration combine.self="override">
+ <archive>
+ <manifestFile>${manifestfile}</manifestFile>
+ <manifestEntries>
+ <Specification-Title>${project.name}</Specification-Title>
+ <Specification-Version>${project.version}</Specification-Version>
+ <Specification-Vendor>${project.organization.name}</Specification-Vendor>
+ <Implementation-Title>${project.name}</Implementation-Title>
+ <Implementation-Version>${project.version}</Implementation-Version>
+ <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+ <Implementation-Vendor-Id>org.apache</Implementation-Vendor-Id>
+ <X-Compile-Source-JDK>${maven.compiler.source}</X-Compile-Source-JDK>
+ <X-Compile-Target-JDK>${maven.compiler.target}</X-Compile-Target-JDK>
+ <Multi-Release>true</Multi-Release>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </execution>
+ <execution>
+ <id>default</id>
+ <goals>
+ <goal>test-jar</goal>
+ </goals>
+ <configuration>
+ <archive>
+ <manifestFile>${manifestfile}</manifestFile>
+ <manifestEntries>
+ <Specification-Title>${project.name}</Specification-Title>
+ <Specification-Version>${project.version}</Specification-Version>
+ <Specification-Vendor>${project.organization.name}</Specification-Vendor>
+ <Implementation-Title>${project.name}</Implementation-Title>
+ <Implementation-Version>${project.version}</Implementation-Version>
+ <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+ <Implementation-Vendor-Id>org.apache</Implementation-Vendor-Id>
+ <X-Compile-Source-JDK>${maven.compiler.source}</X-Compile-Source-JDK>
+ <X-Compile-Target-JDK>${maven.compiler.target}</X-Compile-Target-JDK>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <skip>${maven.test.skip}</skip>
+ <excludes>
+ <exclude>**/JsonTemplateLayoutConcurrentEncodeTest.java</exclude>
+ <exclude>**/JsonTemplateLayoutTest.java</exclude>
+ </excludes>
+ <!-- Enforcing a non-UTF-8 encoding to check that the layout
+ indeed handles everything in UTF-8 without implicitly
+ relying on the system defaults. -->
+ <argLine>-Dfile.encoding=US-ASCII</argLine>
+ </configuration>
+ <executions>
+ <!-- Dummy recycler execution -->
+ <execution>
+ <id>recycler-dummy</id>
+ <goals>
+ <goal>test</goal>
+ </goals>
+ <configuration>
+ <skip>${skipTests}</skip>
+ <systemPropertyVariables>
+ <log4j2.layout.jsonTemplate.recyclerFactory>threadLocal</log4j2.layout.jsonTemplate.recyclerFactory>
+ </systemPropertyVariables>
+ <includes>
+ <include>**/JsonTemplateLayoutConcurrentEncodeTest.java</include>
+ <include>**/JsonTemplateLayoutTest.java</include>
+ </includes>
+ </configuration>
+ </execution>
+ <!-- Thread-Local recycler execution -->
+ <execution>
+ <id>recycler-tl</id>
+ <goals>
+ <goal>test</goal>
+ </goals>
+ <configuration>
+ <skip>${skipTests}</skip>
+ <systemPropertyVariables>
+ <log4j2.layout.jsonTemplate.recyclerFactory>threadLocal</log4j2.layout.jsonTemplate.recyclerFactory>
+ </systemPropertyVariables>
+ <includes>
+ <include>**/JsonTemplateLayoutConcurrentEncodeTest.java</include>
+ <include>**/JsonTemplateLayoutTest.java</include>
+ </includes>
+ </configuration>
+ </execution>
+ <!-- ArrayBlockingQueue recycler execution -->
+ <execution>
+ <id>recycler-abq</id>
+ <goals>
+ <goal>test</goal>
+ </goals>
+ <configuration>
+ <skip>${skipTests}</skip>
+ <systemPropertyVariables>
+ <log4j2.layout.jsonTemplate.recyclerFactory>queue:supplier=java.util.concurrent.ArrayBlockingQueue.new</log4j2.layout.jsonTemplate.recyclerFactory>
+ </systemPropertyVariables>
+ <includes>
+ <include>**/JsonTemplateLayoutConcurrentEncodeTest.java</include>
+ <include>**/JsonTemplateLayoutTest.java</include>
+ </includes>
+ </configuration>
+ </execution>
+ <!-- MpmcArrayQueue recycler execution -->
+ <execution>
+ <id>recycler-mpmc</id>
+ <goals>
+ <goal>test</goal>
+ </goals>
+ <configuration>
+ <skip>${skipTests}</skip>
+ <systemPropertyVariables>
+ <log4j2.layout.jsonTemplate.recyclerFactory>queue:supplier=org.jctools.queues.MpmcArrayQueue.new</log4j2.layout.jsonTemplate.recyclerFactory>
+ </systemPropertyVariables>
+ <includes>
+ <include>**/JsonTemplateLayoutConcurrentEncodeTest.java</include>
+ <include>**/JsonTemplateLayoutTest.java</include>
+ </includes>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ <!-- Disable ITs, which are Docker-dependent, by default. -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>docker</id>
+ <activation>
+ <activeByDefault>false</activeByDefault>
+ </activation>
+ <build>
+ <plugins>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <skip>${skipTests}</skip>
+ <includes>
+ <include>**/*IT.java</include>
+ </includes>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>io.fabric8</groupId>
+ <artifactId>docker-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>start</id>
+ <phase>pre-integration-test</phase>
+ <goals>
+ <goal>start</goal>
+ </goals>
+ </execution>
+ <execution>
+ <id>stop</id>
+ <phase>post-integration-test</phase>
+ <goals>
+ <goal>stop</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <verbose>all</verbose>
+ <startParallel>true</startParallel>
+ <autoCreateCustomNetworks>true</autoCreateCustomNetworks>
+ <images>
+ <image>
+ <alias>elasticsearch</alias>
+ <name>elasticsearch:${elastic.version}</name>
+ <run>
+ <env>
+ <discovery.type>single-node</discovery.type>
+ </env>
+ <ports>
+ <port>9200:9200</port>
+ </ports>
+ <network>
+ <mode>custom</mode>
+ <name>log4j-layout-json-template-network</name>
+ <alias>elasticsearch</alias>
+ </network>
+ <log>
+ <prefix>[ES]</prefix>
+ <color>cyan</color>
+ </log>
+ <wait>
+ <log>recovered \[0\] indices into cluster_state</log>
+ <time>60000</time>
+ </wait>
+ </run>
+ </image>
+ <image>
+ <alias>logstash</alias>
+ <name>logstash:${elastic.version}</name>
+ <run>
+ <dependsOn>
+ <container>elasticsearch</container>
+ </dependsOn>
+ <network>
+ <mode>custom</mode>
+ <name>log4j-layout-json-template-network</name>
+ <alias>logstash</alias>
+ </network>
+ <ports>
+ <port>12222:12222</port>
+ <port>12345:12345</port>
+ </ports>
+ <log>
+ <prefix>[LS]</prefix>
+ <color>green</color>
+ </log>
+ <entrypoint>
+ <exec>
+ <arg>logstash</arg>
+ <arg>--pipeline.batch.size</arg>
+ <arg>1</arg>
+ <arg>-e</arg>
+ <arg>
+ input {
+ gelf {
+ host => "logstash"
+ use_tcp => true
+ use_udp => false
+ port => 12222
+ type => "gelf"
+ }
+ tcp {
+ port => 12345
+ codec => json
+ type => "tcp"
+ }
+ }
+
+ filter {
+ if [type] == "gelf" {
+ # These are GELF/Syslog logging levels as defined in RFC 3164.
+ # Map the integer level to its human readable format.
+ translate {
+ field => "[level]"
+ destination => "[levelName]"
+ dictionary => {
+ "0" => "EMERG"
+ "1" => "ALERT"
+ "2" => "CRITICAL"
+ "3" => "ERROR"
+ "4" => "WARN"
+ "5" => "NOTICE"
+ "6" => "INFO"
+ "7" => "DEBUG"
+ }
+ }
+ }
+ }
+
+ output {
+ # (Un)comment for debugging purposes
+ # stdout { codec => rubydebug }
+ elasticsearch {
+ hosts => ["http://elasticsearch:9200"]
+ index => "log4j"
+ }
+ }
+ </arg>
+ </exec>
+ </entrypoint>
+ <wait>
+ <log>Successfully started Logstash API endpoint</log>
+ <time>60000</time>
+ </wait>
+ </run>
+ </image>
+ </images>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+
+ <reporting>
+ <plugins>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-changes-plugin</artifactId>
+ <version>${changes.plugin.version}</version>
+ <reportSets>
+ <reportSet>
+ <reports>
+ <report>changes-report</report>
+ </reports>
+ </reportSet>
+ </reportSets>
+ <configuration>
+ <issueLinkTemplate>%URL%/show_bug.cgi?id=%ISSUE%</issueLinkTemplate>
+ <useJql>true</useJql>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>${checkstyle.plugin.version}</version>
+ <configuration>
+ <!--<propertiesLocation>${vfs.parent.dir}/checkstyle.properties</propertiesLocation> -->
+ <configLocation>${log4jParentDir}/checkstyle.xml</configLocation>
+ <suppressionsLocation>${log4jParentDir}/checkstyle-suppressions.xml</suppressionsLocation>
+ <enableRulesSummary>false</enableRulesSummary>
+ <propertyExpansion>basedir=${basedir}</propertyExpansion>
+ <propertyExpansion>licensedir=${log4jParentDir}/checkstyle-header.txt</propertyExpansion>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <version>${javadoc.plugin.version}</version>
+ <configuration>
+ <bottom><![CDATA[<p align="center">Copyright © {inceptionYear}-{currentYear} {organizationName}. All Rights Reserved.<br />
+ Apache Logging, Apache Log4j, Log4j, Apache, the Apache feather logo, the Apache Logging project logo,
+ and the Apache Log4j logo are trademarks of The Apache Software Foundation.</p>]]></bottom>
+ <!-- module link generation is completely broken in the javadoc plugin for a multi-module non-aggregating project -->
+ <detectOfflineLinks>false</detectOfflineLinks>
+ <linksource>true</linksource>
+ </configuration>
+ <reportSets>
+ <reportSet>
+ <id>non-aggregate</id>
+ <reports>
+ <report>javadoc</report>
+ </reports>
+ </reportSet>
+ </reportSets>
+ </plugin>
+
+ <plugin>
+ <groupId>com.github.spotbugs</groupId>
+ <artifactId>spotbugs-maven-plugin</artifactId>
+ <configuration>
+ <fork>true</fork>
+ <jvmArgs>-Duser.language=en</jvmArgs>
+ <threshold>Normal</threshold>
+ <effort>Default</effort>
+ <excludeFilterFile>${log4jParentDir}/spotbugs-exclude-filter.xml</excludeFilterFile>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jxr-plugin</artifactId>
+ <version>${jxr.plugin.version}</version>
+ <reportSets>
+ <reportSet>
+ <id>non-aggregate</id>
+ <reports>
+ <report>jxr</report>
+ </reports>
+ </reportSet>
+ <reportSet>
+ <id>aggregate</id>
+ <reports>
+ <report>aggregate</report>
+ </reports>
+ </reportSet>
+ </reportSets>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-pmd-plugin</artifactId>
+ <version>${pmd.plugin.version}</version>
+ <configuration>
+ <targetJdk>${maven.compiler.target}</targetJdk>
+ </configuration>
+ </plugin>
+
+ </plugins>
+ </reporting>
+
+</project>
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java
new file mode 100644
index 0000000..557741d
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayout.java
@@ -0,0 +1,543 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.json.template;
+
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.StringLayout;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
+import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
+import org.apache.logging.log4j.core.config.plugins.PluginElement;
+import org.apache.logging.log4j.core.layout.ByteBufferDestination;
+import org.apache.logging.log4j.core.layout.Encoder;
+import org.apache.logging.log4j.core.layout.LockingStringBuilderEncoder;
+import org.apache.logging.log4j.core.lookup.StrSubstitutor;
+import org.apache.logging.log4j.core.util.Constants;
+import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.core.util.StringEncoder;
+import org.apache.logging.log4j.layout.json.template.resolver.EventResolverContext;
+import org.apache.logging.log4j.layout.json.template.resolver.StackTraceElementObjectResolverContext;
+import org.apache.logging.log4j.layout.json.template.resolver.TemplateResolver;
+import org.apache.logging.log4j.layout.json.template.resolver.TemplateResolvers;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.layout.json.template.util.Recycler;
+import org.apache.logging.log4j.layout.json.template.util.RecyclerFactory;
+import org.apache.logging.log4j.layout.json.template.util.Uris;
+import org.apache.logging.log4j.plugins.Node;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.util.Strings;
+
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+@Plugin(name = "JsonTemplateLayout",
+ category = Node.CATEGORY,
+ elementType = Layout.ELEMENT_TYPE,
+ printObject = true)
+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;
+ this.eventDelimiter = builder.eventDelimiter;
+ 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 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 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");
+ }
+ 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 KeyValuePair[] additionalFields;
+
+ private EventTemplateAdditionalFields(final Builder builder) {
+ this.additionalFields = builder.additionalFields;
+ }
+
+ public KeyValuePair[] getAdditionalFields() {
+ return 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 KeyValuePair[] additionalFields;
+
+ private Builder() {}
+
+ public KeyValuePair[] getAdditionalFields() {
+ return additionalFields;
+ }
+
+ public Builder setAdditionalFields(final KeyValuePair[] additionalFields) {
+ this.additionalFields = additionalFields;
+ return this;
+ }
+
+ @Override
+ public EventTemplateAdditionalFields build() {
+ return new EventTemplateAdditionalFields(this);
+ }
+
+ }
+
+ }
+
+}
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..c156e69
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutDefaults.java
@@ -0,0 +1,204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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:JsonLayout.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 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 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/ContextDataResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolver.java
new file mode 100644
index 0000000..71c973b
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolver.java
@@ -0,0 +1,303 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.StringParameterParser;
+import org.apache.logging.log4j.util.ReadOnlyStringMap;
+import org.apache.logging.log4j.util.TriConsumer;
+
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * Add Mapped Diagnostic Context (MDC).
+ */
+final class ContextDataResolver implements EventResolver {
+
+ private enum Param {;
+
+ private static final String FLATTEN = "flatten";
+
+ private static final String PATTERN = "pattern";
+
+ private static final String KEY = "key";
+
+ private static final String STRINGIFY = "stringify";
+
+ }
+
+ private static final Set<String> PARAMS =
+ new LinkedHashSet<>(Arrays.asList(
+ Param.FLATTEN,
+ Param.PATTERN,
+ Param.KEY,
+ Param.STRINGIFY));
+
+ private final EventResolver internalResolver;
+
+ ContextDataResolver(final EventResolverContext context, final String spec) {
+ this.internalResolver = createResolver(context, spec);
+ }
+
+ private static EventResolver createResolver(
+ final EventResolverContext context,
+ final String spec) {
+ final Map<String, StringParameterParser.Value> params = StringParameterParser.parse(spec, PARAMS);
+ final StringParameterParser.Value keyValue = params.get(Param.KEY);
+ if (keyValue != null) {
+ if (params.size() != 1) {
+ throw new IllegalArgumentException(
+ "MDC key access doesn't take arguments: " + spec);
+ }
+ if (keyValue instanceof StringParameterParser.NullValue) {
+ throw new IllegalArgumentException("missing MDC key: " + spec);
+ }
+ final String key = keyValue.toString();
+ return createKeyResolver(key);
+ } else {
+ return createResolver(context, spec, params);
+ }
+ }
+
+ private static EventResolver createKeyResolver(final String key) {
+ 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);
+ jsonWriter.writeValue(value);
+ }
+
+ };
+ }
+
+ private static EventResolver createResolver(
+ final EventResolverContext context,
+ final String spec,
+ final Map<String, StringParameterParser.Value> params) {
+
+ // Read the flatten prefix.
+ final StringParameterParser.Value flattenValue = params.get(Param.FLATTEN);
+ final boolean flatten;
+ final String prefix;
+ if (flattenValue != null) {
+ flatten = true;
+ prefix = flattenValue.toString();
+ } else {
+ flatten = false;
+ prefix = null;
+ }
+
+ // Read the pattern.
+ final StringParameterParser.Value patternValue = params.get(Param.PATTERN);
+ final Pattern pattern;
+ if (patternValue == null) {
+ pattern = null;
+ } else if (patternValue instanceof StringParameterParser.NullValue) {
+ throw new IllegalArgumentException("missing MDC pattern: " + spec);
+ } else {
+ pattern = Pattern.compile(patternValue.toString());
+ }
+
+ // Read the stringify flag.
+ final StringParameterParser.Value stringifyValue = params.get(Param.STRINGIFY);
+ final boolean stringify;
+ if (stringifyValue == null) {
+ stringify = false;
+ } else if (!(stringifyValue instanceof StringParameterParser.NullValue)) {
+ throw new IllegalArgumentException(
+ "MDC stringify directive doesn't take parameters: " + spec);
+ } else {
+ stringify = true;
+ }
+
+ // Create the recycler for the loop context.
+ final Recycler<LoopContext> loopContextRecycler =
+ context.getRecyclerFactory().create(() -> {
+ final LoopContext loopContext = new LoopContext();
+ if (prefix != null) {
+ loopContext.prefix = prefix;
+ loopContext.prefixedKey = new StringBuilder(prefix);
+ }
+ loopContext.pattern = pattern;
+ loopContext.stringify = stringify;
+ 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 stringify;
+
+ 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.stringify && !(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/ContextDataResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolverFactory.java
new file mode 100644
index 0000000..c18c2a6
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextDataResolverFactory.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;
+
+final class ContextDataResolverFactory implements EventResolverFactory<ContextDataResolver> {
+
+ private static final ContextDataResolverFactory INSTANCE = new ContextDataResolverFactory();
+
+ private ContextDataResolverFactory() {}
+
+ static ContextDataResolverFactory getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public String getName() {
+ return ContextDataResolver.getName();
+ }
+
+ @Override
+ public ContextDataResolver create(final EventResolverContext context, final String key) {
+ return new ContextDataResolver(context, key);
+ }
+
+}
diff --git a/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolver.java
new file mode 100644
index 0000000..1b5fb9f
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolver.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 org.apache.logging.log4j.layout.json.template.util.StringParameterParser;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * Add Nested Diagnostic Context (NDC).
+ */
+final class ContextStackResolver implements EventResolver {
+
+ private final Pattern itemPattern;
+
+ private enum Param {;
+
+ private static final String PATTERN = "pattern";
+
+ }
+
+ ContextStackResolver(final String spec) {
+ final Map<String, StringParameterParser.Value> params =
+ StringParameterParser.parse(spec, Collections.singleton(Param.PATTERN));
+ final StringParameterParser.Value patternValue = params.get(Param.PATTERN);
+ if (patternValue == null) {
+ this.itemPattern = null;
+ } else if (patternValue instanceof StringParameterParser.NullValue) {
+ throw new IllegalArgumentException("missing NDC pattern: " + spec);
+ } else {
+ final String pattern = patternValue.toString();
+ try {
+ this.itemPattern = Pattern.compile(pattern);
+ } catch (final PatternSyntaxException error) {
+ throw new IllegalArgumentException(
+ "invalid NDC pattern: " + spec, error);
+ }
+ }
+ }
+
+ 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/ContextStackResolverFactory.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolverFactory.java
new file mode 100644
index 0000000..0db832a
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ContextStackResolverFactory.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;
+
+final class ContextStackResolverFactory implements EventResolverFactory<ContextStackResolver> {
+
+ private static final ContextStackResolverFactory INSTANCE = new ContextStackResolverFactory();
+
+ private ContextStackResolverFactory() {}
+
+ static ContextStackResolverFactory getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public String getName() {
+ return ContextStackResolver.getName();
+ }
+
+ @Override
+ public ContextStackResolver create(final EventResolverContext context, final String key) {
+ return new ContextStackResolver(key);
+ }
+
+}
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..c55fc58
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EndOfBatchResolverFactory.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;
+
+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, String key) {
+ 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..e178a66
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/EventResolverContext.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.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 KeyValuePair[] 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;
+ }
+
+ KeyValuePair[] 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 KeyValuePair[] 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 KeyValuePair[] 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..86ea6bb
--- /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(
+ ContextDataResolverFactory.getInstance(),
+ ContextStackResolverFactory.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..de1ec31
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionInternalResolverFactory.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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;
+
+abstract class ExceptionInternalResolverFactory {
+
+ private static final EventResolver NULL_RESOLVER =
+ (ignored, jsonGenerator) -> jsonGenerator.writeNull();
+
+ EventResolver createInternalResolver(
+ final EventResolverContext context,
+ final String key) {
+
+ // Split the key into its major and minor components.
+ String majorKey;
+ String minorKey;
+ final int colonIndex = key.indexOf(':');
+ if (colonIndex >= 0) {
+ majorKey = key.substring(0, colonIndex);
+ minorKey = key.substring(colonIndex + 1);
+ } else {
+ majorKey = key;
+ minorKey = "";
+ }
+
+ // Create the resolver.
+ switch (majorKey) {
+ case "className": return createClassNameResolver();
+ case "message": return createMessageResolver(context);
+ case "stackTrace": return createStackTraceResolver(context, minorKey);
+ }
+ throw new IllegalArgumentException("unknown key: " + key);
+
+ }
+
+ abstract EventResolver createClassNameResolver();
+
+ abstract EventResolver createMessageResolver(final EventResolverContext context);
+
+ private EventResolver createStackTraceResolver(
+ final EventResolverContext context,
+ final String minorKey) {
+ if (!context.isStackTraceEnabled()) {
+ return NULL_RESOLVER;
+ }
+ switch (minorKey) {
+ case "text": return createStackTraceTextResolver(context);
+ case "": return createStackTraceObjectResolver(context);
+ }
+ throw new IllegalArgumentException("unknown minor key: " + minorKey);
+ }
+
+ abstract EventResolver createStackTraceTextResolver(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..a5ab172
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolver.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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;
+
+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 createStackTraceTextResolver(final EventResolverContext context) {
+ StackTraceTextResolver stackTraceTextResolver =
+ new StackTraceTextResolver(context);
+ return (final LogEvent logEvent, final JsonWriter jsonWriter) -> {
+ final Throwable exception = logEvent.getThrown();
+ if (exception == null) {
+ jsonWriter.writeNull();
+ } else {
+ stackTraceTextResolver.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 String key) {
+ this.stackTraceEnabled = context.isStackTraceEnabled();
+ this.internalResolver = INTERNAL_RESOLVER_FACTORY.createInternalResolver(context, key);
+ }
+
+ 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..8a70bfd
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionResolverFactory.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;
+
+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 String key) {
+ return new ExceptionResolver(context, key);
+ }
+
+}
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..230ac59
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolver.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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;
+
+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 createStackTraceTextResolver(final EventResolverContext context) {
+ final StackTraceTextResolver stackTraceTextResolver =
+ new StackTraceTextResolver(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);
+ stackTraceTextResolver.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 String key) {
+ this.stackTraceEnabled = context.isStackTraceEnabled();
+ this.internalResolver = INTERNAL_RESOLVER_FACTORY.createInternalResolver(context, key);
+ }
+
+ 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..1f625b0
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ExceptionRootCauseResolverFactory.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;
+
+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 String key) {
+ return new ExceptionRootCauseResolver(context, key);
+ }
+
+}
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..ad33a0a
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolver.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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;
+
+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 String key) {
+ final JsonWriter jsonWriter = context.getJsonWriter();
+ if (key == null) {
+ internalResolver = createNameResolver(jsonWriter);
+ } else if ("severity".equals(key)) {
+ internalResolver = createSeverityNameResolver(jsonWriter);
+ } else if ("severity:code".equals(key)) {
+ internalResolver = SEVERITY_CODE_RESOLVER;
+ } else {
+ throw new IllegalArgumentException("unknown key: " + key);
+ }
+ }
+
+ 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 createSeverityNameResolver(
+ final JsonWriter contextJsonWriter) {
+ final Map<Level, String> resolutionByLevel = Arrays
+ .stream(Level.values())
+ .collect(Collectors.toMap(
+ Function.identity(),
+ (final Level level) -> contextJsonWriter.use(() -> {
+ final String severityName = Severity.getSeverity(level).name();
+ contextJsonWriter.writeString(severityName);
+ })));
+ 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..e28745b
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LevelResolverFactory.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;
+
+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 String key) {
+ return new LevelResolver(context, key);
+ }
+
+}
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..b88bd0d
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolver.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.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+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 String key) {
+ this.internalResolver = createInternalResolver(key);
+ }
+
+ private static EventResolver createInternalResolver(final String key) {
+ switch (key) {
+ case "name": return NAME_RESOLVER;
+ case "fqcn": return FQCN_RESOLVER;
+ }
+ throw new IllegalArgumentException("unknown key: " + key);
+ }
+
+ 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..10d4ba7
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/LoggerResolverFactory.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;
+
+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 String key) {
+ return new LoggerResolver(key);
+ }
+
+}
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..29be9e8
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolver.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.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;
+
+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 String key) {
+ this.key = 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..60262d4
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MainMapResolverFactory.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;
+
+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 String key) {
+ return new MainMapResolver(key);
+ }
+
+}
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..1c71619
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolver.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.MapLookup;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.message.MapMessage;
+
+final class MapResolver implements EventResolver {
+
+ private static final MapLookup MAP_LOOKUP = new MapLookup();
+
+ private final String key;
+
+ static String getName() {
+ return "map";
+ }
+
+ MapResolver(final String key) {
+ this.key = key;
+ }
+
+ @Override
+ public boolean isResolvable(final LogEvent logEvent) {
+ return logEvent.getMessage() instanceof MapMessage;
+ }
+
+ @Override
+ public void resolve(
+ final LogEvent logEvent,
+ final JsonWriter jsonWriter) {
+
+ // If the event message is not of type MapMessage, then do not even try
+ // to perform the map lookup.
+ if (!(logEvent.getMessage() instanceof MapMessage)) {
+ jsonWriter.writeNull();
+ }
+
+ // Perform the map lookup against Log4j.
+ else {
+ final String resolvedValue = MAP_LOOKUP.lookup(logEvent, key);
+ jsonWriter.writeString(resolvedValue);
+ }
+
+ }
+
+}
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..6e2e8d6
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MapResolverFactory.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;
+
+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 String key) {
+ return new MapResolver(key);
+ }
+
+}
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..86a352f
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolver.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.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;
+
+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 String key) {
+ this.internalResolver = createInternalResolver(key);
+ }
+
+ private TemplateResolver<LogEvent> createInternalResolver(final String key) {
+ if ("name".equals(key)) {
+ return NAME_RESOLVER;
+ }
+ throw new IllegalArgumentException("unknown key: " + key);
+ }
+
+ 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..42f20c5
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MarkerResolverFactory.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;
+
+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 String key) {
+ return new MarkerResolver(key);
+ }
+
+}
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..b2953df
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolver.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.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;
+import org.apache.logging.log4j.util.Strings;
+
+final class MessageResolver implements EventResolver {
+
+ private static final String[] FORMATS = { "JSON" };
+
+ private final EventResolver internalResolver;
+
+ MessageResolver(final String key) {
+ this.internalResolver = createInternalResolver(key);
+ }
+
+ static String getName() {
+ return "message";
+ }
+
+ private static EventResolver createInternalResolver(final String key) {
+ if (Strings.isBlank(key)) {
+ return MessageResolver::resolveText;
+ } else if (FORMATS[0].equalsIgnoreCase(key)) {
+ return MessageResolver::resolveJson;
+ } else {
+ throw new IllegalArgumentException("unknown key: " + key);
+ }
+ }
+
+ @Override
+ public void resolve(
+ final LogEvent logEvent,
+ final JsonWriter jsonWriter) {
+ internalResolver.resolve(logEvent, jsonWriter);
+ }
+
+ private static void resolveText(
+ final LogEvent logEvent,
+ final JsonWriter jsonWriter) {
+ final Message message = logEvent.getMessage();
+ resolveText(message, jsonWriter);
+ }
+
+ private static void resolveText(
+ final Message message,
+ final JsonWriter jsonWriter) {
+ if (message instanceof StringBuilderFormattable) {
+ final StringBuilderFormattable formattable =
+ (StringBuilderFormattable) message;
+ jsonWriter.writeString(formattable);
+ } else {
+ final String formattedMessage = message.getFormattedMessage();
+ jsonWriter.writeString(formattedMessage);
+ }
+ }
+
+ private static void resolveJson(
+ final LogEvent logEvent,
+ final JsonWriter jsonWriter) {
+
+ // Try SimpleMessage serializer.
+ final Message message = logEvent.getMessage();
+ if (writeSimpleMessage(jsonWriter, message)) {
+ return;
+ }
+
+ // Try MultiformatMessage serializer.
+ if (writeMultiformatMessage(jsonWriter, message)) {
+ return;
+ }
+
+ // Try ObjectMessage serializer.
+ if (writeObjectMessage(jsonWriter, message)) {
+ return;
+ }
+
+ // Fallback to plain Object write.
+ resolveText(logEvent, jsonWriter);
+
+ }
+
+ private static boolean writeSimpleMessage(
+ final JsonWriter jsonWriter,
+ final Message message) {
+
+ // Check type.
+ if (!(message instanceof SimpleMessage)) {
+ return false;
+ }
+ final SimpleMessage simpleMessage = (SimpleMessage) message;
+
+ // Write message.
+ final String formattedMessage = simpleMessage.getFormattedMessage();
+ jsonWriter.writeString(formattedMessage);
+ return true;
+
+ }
+
+ 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;
+ }
+ }
+
+ // Write the formatted JSON, if supported.
+ if (jsonSupported) {
+ final String messageJson = multiformatMessage.getFormattedMessage(FORMATS);
+ jsonWriter.writeRawString(messageJson);
+ return true;
+ }
+
+ // Fallback to the default message formatter.
+ resolveText((LogEvent) message, jsonWriter);
+ 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..9032d9e
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/MessageResolverFactory.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;
+
+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 String key) {
+ return new MessageResolver(key);
+ }
+
+}
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..1b2fba0
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolver.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.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;
+
+final class PatternResolver implements EventResolver {
+
+ private final BiConsumer<StringBuilder, LogEvent> emitter;
+
+ PatternResolver(
+ final EventResolverContext context,
+ final String key) {
+ final PatternLayout patternLayout = PatternLayout
+ .newBuilder()
+ .setConfiguration(context.getConfiguration())
+ .setCharset(context.getCharset())
+ .setPattern(key)
+ .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..c47f850
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/PatternResolverFactory.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;
+
+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 String key) {
+ return new PatternResolver(context, key);
+ }
+
+}
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..7a029f1
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolver.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 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 String key) {
+ this.locationInfoEnabled = context.isLocationInfoEnabled();
+ this.internalResolver = createInternalResolver(context, key);
+ }
+
+ private static EventResolver createInternalResolver(
+ final EventResolverContext context,
+ final String key) {
+ if (!context.isLocationInfoEnabled()) {
+ return NULL_RESOLVER;
+ }
+ switch (key) {
+ 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 key: " + key);
+ }
+
+ 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..0c5c2b5
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/SourceResolverFactory.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;
+
+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 String key) {
+ return new SourceResolver(context, key);
+ }
+
+}
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..1e30204
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceElementObjectResolver.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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 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 String key) {
+ this.internalResolver = createInternalResolver(key);
+ }
+
+ private TemplateResolver<StackTraceElement> createInternalResolver(final String key) {
+ switch (key) {
+ 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 key: " + key);
+ }
+
+ 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..84456e7
--- /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 String key) {
+ return new StackTraceElementObjectResolver(key);
+ }
+
+}
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/StackTraceTextResolver.java b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceTextResolver.java
new file mode 100644
index 0000000..17f9ac1
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/StackTraceTextResolver.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 StackTraceTextResolver implements StackTraceResolver {
+
+ private final Recycler<TruncatingBufferedPrintWriter> writerRecycler;
+
+ StackTraceTextResolver(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/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..dacc07b
--- /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, String key);
+
+}
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..0ba10ce
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TemplateResolvers.java
@@ -0,0 +1,355 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.KeyValuePair;
+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 TemplateResolver<?> EMPTY_ARRAY_RESOLVER =
+ (final Object ignored, final JsonWriter jsonWriter) -> {
+ jsonWriter.writeArrayStart();
+ jsonWriter.writeArrayEnd();
+ };
+
+ private static final TemplateResolver<?> EMPTY_OBJECT_RESOLVER =
+ (final Object ignored, final JsonWriter jsonWriter) -> {
+ jsonWriter.writeObjectStart();
+ jsonWriter.writeObjectEnd();
+ };
+
+ private static final TemplateResolver<?> NULL_RESOLVER =
+ (final Object ignored, 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 KeyValuePair[] additionalFields = eventResolverContext.getAdditionalFields();
+ if (additionalFields != null) {
+
+ // 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 (class=%s)",
+ node.getClass().getName());
+ throw new IllegalArgumentException(message);
+ }
+
+ // Merge additional fields.
+ for (final KeyValuePair additionalField : additionalFields) {
+ objectNode.put(additionalField.getKey(), additionalField.getValue());
+ }
+
+ }
+ }
+
+ // Resolve the template.
+ return ofObject(context, node);
+
+ }
+
+ 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) {
+
+ // 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());
+
+ // Create a parent resolver collecting each object field resolver execution.
+ return (value, 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;
+ if (fieldResolver.isFlattening()) {
+ 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> ofString(
+ final C context,
+ final String fieldValue) {
+
+ // Try to resolve the directive as a ${json:xxx} parameter.
+ final TemplateResolverRequest resolverRequest = readResolverRequest(fieldValue);
+ if (resolverRequest != null) {
+ final TemplateResolverFactory<V, C, ? extends TemplateResolver<V>> resolverFactory =
+ context.getResolverFactoryByName().get(resolverRequest.resolverName);
+ if (resolverFactory != null) {
+ return resolverFactory.create(context, resolverRequest.resolverKey);
+ }
+ }
+
+ // The rest is the fallback template resolver that delegates every other
+ // substitution to Log4j. This will be the case for every template value
+ // that does not use directives of pattern ${json:xxx}. This
+ // additionally serves as a mechanism to resolve values at runtime when
+ // this layout misses certain resolvers.
+
+ // 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);
+ if (replacedText == null) {
+ jsonWriter.writeNull();
+ } else {
+ 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. (Blank value check has already been done at the top.)
+ else {
+ final String escapedFieldValue =
+ contextJsonWriter.use(() ->
+ contextJsonWriter.writeString(fieldValue));
+ return (final V value, final JsonWriter jsonWriter) ->
+ jsonWriter.writeRawString(escapedFieldValue);
+ }
+
+ }
+
+ private static TemplateResolverRequest readResolverRequest(final String fieldValue) {
+
+ // Bail-out if cannot spot the template signature.
+ if (!fieldValue.startsWith("${json:") || !fieldValue.endsWith("}")) {
+ return null;
+ }
+
+ // Try to read both resolver name and key.
+ final int resolverNameStartIndex = 7;
+ final int fieldNameSeparatorIndex = fieldValue.indexOf(':', resolverNameStartIndex);
+ if (fieldNameSeparatorIndex < 0) {
+ final int resolverNameEndIndex = fieldValue.length() - 1;
+ final String resolverName = fieldValue.substring(resolverNameStartIndex, resolverNameEndIndex);
+ return new TemplateResolverRequest(resolverName, null);
+ } else {
+ @SuppressWarnings("UnnecessaryLocalVariable")
+ final int resolverNameEndIndex = fieldNameSeparatorIndex;
+ final int resolverKeyStartIndex = fieldNameSeparatorIndex + 1;
+ final int resolverKeyEndIndex = fieldValue.length() - 1;
+ final String resolverName = fieldValue.substring(resolverNameStartIndex, resolverNameEndIndex);
+ final String resolverKey = fieldValue.substring(resolverKeyStartIndex, resolverKeyEndIndex);
+ return new TemplateResolverRequest(resolverName, resolverKey);
+ }
+
+ }
+
+ private static final class TemplateResolverRequest {
+
+ private final String resolverName;
+
+ private final String resolverKey;
+
+ private TemplateResolverRequest(final String resolverName, final String resolverKey) {
+ this.resolverName = resolverName;
+ this.resolverKey = resolverKey;
+ }
+
+ }
+
+ 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/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..afe8ecb
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolver.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;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+
+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 String key) {
+ this.internalResolver = createInternalResolver(key);
+ }
+
+ private static EventResolver createInternalResolver(final String key) {
+ switch (key) {
+ case "name": return NAME_RESOLVER;
+ case "id": return ID_RESOLVER;
+ case "priority": return PRIORITY_RESOLVER;
+ }
+ throw new IllegalArgumentException("unknown key: " + key);
+ }
+
+ 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..266c982
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/ThreadResolverFactory.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;
+
+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 String key) {
+ return new ThreadResolver(key);
+ }
+
+}
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..c7c32b0
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/resolver/TimestampResolver.java
@@ -0,0 +1,446 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.json.template.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.time.Instant;
+import org.apache.logging.log4j.core.time.internal.format.FastDateFormat;
+import org.apache.logging.log4j.layout.json.template.JsonTemplateLayoutDefaults;
+import org.apache.logging.log4j.layout.json.template.util.JsonWriter;
+import org.apache.logging.log4j.layout.json.template.util.StringParameterParser;
+
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+
+final class TimestampResolver implements EventResolver {
+
+ private final EventResolver internalResolver;
+
+ TimestampResolver(final String key) {
+ this.internalResolver = (key != null && key.startsWith("epoch:"))
+ ? createEpochResolver(key)
+ : createFormatResolver(key);
+ }
+
+ /**
+ * Context for GC-free formatted timestamp resolver.
+ */
+ private static final class FormatResolverContext {
+
+ private enum Key {;
+
+ private static final String PATTERN = "pattern";
+
+ private static final String TIME_ZONE = "timeZone";
+
+ private static final String LOCALE = "locale";
+
+ }
+
+ private static final Set<String> KEYS =
+ new LinkedHashSet<>(Arrays.asList(
+ Key.PATTERN, Key.TIME_ZONE, Key.LOCALE));
+
+ 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 fromKey(final String key) {
+ final Map<String, StringParameterParser.Value> keys =
+ StringParameterParser.parse(key, KEYS);
+ final String pattern = readPattern(keys);
+ final TimeZone timeZone = readTimeZone(keys);
+ final Locale locale = readLocale(keys);
+ final FastDateFormat fastDateFormat =
+ FastDateFormat.getInstance(pattern, timeZone, locale);
+ return new FormatResolverContext(timeZone, locale, fastDateFormat);
+ }
+
+ private static String readPattern(
+ final Map<String, StringParameterParser.Value> keys) {
+ final StringParameterParser.Value patternValue = keys.get(Key.PATTERN);
+ if (patternValue == null || patternValue instanceof StringParameterParser.NullValue) {
+ return JsonTemplateLayoutDefaults.getTimestampFormatPattern();
+ }
+ final String pattern = patternValue.toString();
+ try {
+ FastDateFormat.getInstance(pattern);
+ } catch (final IllegalArgumentException error) {
+ throw new IllegalArgumentException(
+ "invalid pattern in timestamp key: " + pattern,
+ error);
+ }
+ return pattern;
+ }
+
+ private static TimeZone readTimeZone(
+ final Map<String, StringParameterParser.Value> keys) {
+ final StringParameterParser.Value timeZoneValue = keys.get(Key.TIME_ZONE);
+ if (timeZoneValue == null || timeZoneValue instanceof StringParameterParser.NullValue) {
+ return JsonTemplateLayoutDefaults.getTimeZone();
+ }
+ final String timeZoneId = timeZoneValue.toString();
+ boolean found = false;
+ for (final String availableTimeZone : TimeZone.getAvailableIDs()) {
+ if (availableTimeZone.equalsIgnoreCase(timeZoneId)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ throw new IllegalArgumentException(
+ "invalid time zone in timestamp key: " + timeZoneId);
+ }
+ return TimeZone.getTimeZone(timeZoneId);
+ }
+
+ private static Locale readLocale(
+ final Map<String, StringParameterParser.Value> keys) {
+ final StringParameterParser.Value localeValue = keys.get(Key.LOCALE);
+ if (localeValue == null || localeValue instanceof StringParameterParser.NullValue) {
+ return JsonTemplateLayoutDefaults.getLocale();
+ }
+ final String locale = localeValue.toString();
+ 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 in timestamp key: " + locale);
+ }
+ }
+
+ }
+
+ /**
+ * 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 String key) {
+ final FormatResolverContext formatResolverContext =
+ FormatResolverContext.fromKey(key);
+ return new FormatResolver(formatResolverContext);
+ }
+
+ private static EventResolver createEpochResolver(final String key) {
+ switch (key) {
+ case "epoch:nanos":
+ return createEpochNanosResolver();
+ case "epoch:micros":
+ return createEpochMicrosDoubleResolver();
+ case "epoch:micros,integral":
+ return createEpochMicrosLongResolver();
+ case "epoch:millis":
+ return createEpochMillisDoubleResolver();
+ case "epoch:millis,integral":
+ return createEpochMillisLongResolver();
+ case "epoch:secs":
+ return createEpochSecsDoubleResolver();
+ case "epoch:secs,integral":
+ return createEpochSecsLongResolver();
+ case "epoch:micros.nanos":
+ return createEpochMicrosNanosResolver();
+ case "epoch:millis.nanos":
+ return createEpochMillisNanosResolver();
+ case "epoch:millis.micros":
+ return createEpochMillisMicrosResolver();
+ case "epoch:secs.nanos":
+ return createEpochSecsNanosResolver();
+ case "epoch:secs.micros":
+ return createEpochSecsMicrosResolver();
+ case "epoch:secs.millis":
+ return createEpochSecsMillisResolver();
+ default:
+ throw new IllegalArgumentException(
+ "was expecting an epoch key: " + key);
+ }
+ }
+
+ private static final int MICROS_PER_SEC = 1_000_000;
+
+ private static final int NANOS_PER_SEC = 1_000_000_000;
+
+ private static final int NANOS_PER_MILLI = 1_000_000;
+
+ private static final int NANOS_PER_MICRO = 1_000;
+
+ 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 EventResolver createEpochNanosResolver() {
+ return new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ final long nanos = epochNanos(logEventInstant);
+ jsonWriter.writeNumber(nanos);
+ }
+ };
+ }
+
+ private static EventResolver createEpochMicrosDoubleResolver() {
+ return new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ final long secs = logEventInstant.getEpochSecond();
+ final int nanosOfSecs = logEventInstant.getNanoOfSecond();
+ final long micros = MICROS_PER_SEC * secs + nanosOfSecs / NANOS_PER_MICRO;
+ final int nanosOfMicros = nanosOfSecs - nanosOfSecs % NANOS_PER_MICRO;
+ jsonWriter.writeNumber(micros, nanosOfMicros);
+ }
+ };
+ }
+
+ private static EventResolver createEpochMicrosLongResolver() {
+ return new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ final long nanos = epochNanos(logEventInstant);
+ final long micros = nanos / NANOS_PER_MICRO;
+ jsonWriter.writeNumber(micros);
+ }
+ };
+ }
+
+ private static EventResolver createEpochMillisDoubleResolver() {
+ return new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ jsonWriter.writeNumber(
+ logEventInstant.getEpochMillisecond(),
+ logEventInstant.getNanoOfMillisecond());
+ }
+ };
+ }
+
+ private static EventResolver createEpochMillisLongResolver() {
+ return new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ jsonWriter.writeNumber(logEventInstant.getEpochMillisecond());
+ }
+ };
+ }
+
+ private static EventResolver createEpochSecsDoubleResolver() {
+ return new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ jsonWriter.writeNumber(
+ logEventInstant.getEpochSecond(),
+ logEventInstant.getNanoOfSecond());
+ }
+ };
+ }
+
+ private static EventResolver createEpochSecsLongResolver() {
+ return new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ jsonWriter.writeNumber(logEventInstant.getEpochSecond());
+ }
+ };
+ }
+
+ private static EventResolver createEpochMicrosNanosResolver() {
+ return new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ final int nanosOfSecs = logEventInstant.getNanoOfSecond();
+ final int nanosOfMicros = nanosOfSecs % NANOS_PER_MICRO;
+ jsonWriter.writeNumber(nanosOfMicros);
+ }
+ };
+ }
+
+ private static EventResolver createEpochMillisNanosResolver() {
+ return new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ jsonWriter.writeNumber(logEventInstant.getNanoOfMillisecond());
+ }
+ };
+ }
+
+ private static EventResolver createEpochMillisMicrosResolver() {
+ return new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ final int nanosOfMillis = logEventInstant.getNanoOfMillisecond();
+ final int microsOfMillis = nanosOfMillis / NANOS_PER_MICRO;
+ jsonWriter.writeNumber(microsOfMillis);
+ }
+ };
+ }
+
+ private static EventResolver createEpochSecsNanosResolver() {
+ return new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ jsonWriter.writeNumber(logEventInstant.getNanoOfSecond());
+ }
+ };
+ }
+
+ private static EventResolver createEpochSecsMicrosResolver() {
+ return new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ final int nanosOfSecs = logEventInstant.getNanoOfSecond();
+ final int microsOfSecs = nanosOfSecs / NANOS_PER_MICRO;
+ jsonWriter.writeNumber(microsOfSecs);
+ }
+ };
+ }
+
+ private static EventResolver createEpochSecsMillisResolver() {
+ return new EpochResolver() {
+ @Override
+ void resolve(final Instant logEventInstant, final JsonWriter jsonWriter) {
+ final int nanosOfSecs = logEventInstant.getNanoOfSecond();
+ final int millisOfSecs = nanosOfSecs / NANOS_PER_MILLI;
+ jsonWriter.writeNumber(millisOfSecs);
+ }
+ };
+ }
+
+ private static long epochNanos(Instant instant) {
+ return NANOS_PER_SEC * 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..638df83
--- /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 String key) {
+ return new TimestampResolver(key);
+ }
+
+}
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/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..a6937c6
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/RecyclerFactories.java
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.json.template.util;
+
+import org.apache.logging.log4j.core.util.Constants;
+import org.apache.logging.log4j.plugins.Plugin;
+import org.apache.logging.log4j.plugins.convert.TypeConverter;
+import org.apache.logging.log4j.plugins.convert.TypeConverters;
+import org.apache.logging.log4j.util.LoaderUtil;
+import org.jctools.queues.MpmcArrayQueue;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.function.Supplier;
+
+public enum RecyclerFactories {;
+
+ private static final String JCTOOLS_QUEUE_CLASS_SUPPLIER_PATH =
+ "org.jctools.queues.MpmcArrayQueue.new";
+
+ private static final boolean JCTOOLS_QUEUE_CLASS_AVAILABLE =
+ isJctoolsQueueClassAvailable();
+
+ private static boolean isJctoolsQueueClassAvailable() {
+ try {
+ final String className = JCTOOLS_QUEUE_CLASS_SUPPLIER_PATH
+ .replaceAll("\\.new$", "");
+ LoaderUtil.loadClass(className);
+ return true;
+ } catch (final ClassNotFoundException ignored) {
+ return false;
+ }
+ }
+
+ @Plugin(name = "RecyclerFactory", category = TypeConverters.CATEGORY)
+ public static final class RecyclerFactoryConverter implements TypeConverter<RecyclerFactory> {
+ @Override
+ public RecyclerFactory convert(final String recyclerFactorySpec) {
+ return ofSpec(recyclerFactorySpec);
+ }
+ }
+
+ public static RecyclerFactory ofSpec(final String recyclerFactorySpec) {
+
+ // Determine the default capacity.
+ int defaultCapacity = Math.max(
+ 2 * Runtime.getRuntime().availableProcessors() + 1,
+ 8);
+
+ // TLA-, MPMC-, or ABQ-based queueing factory -- if nothing is specified.
+ if (recyclerFactorySpec == null) {
+ if (Constants.ENABLE_THREADLOCALS) {
+ return ThreadLocalRecyclerFactory.getInstance();
+ } else {
+ final Supplier<Queue<Object>> queueSupplier =
+ JCTOOLS_QUEUE_CLASS_AVAILABLE
+ ? () -> new MpmcArrayQueue<>(defaultCapacity)
+ : () -> new ArrayBlockingQueue<>(defaultCapacity);
+ return new QueueingRecyclerFactory(queueSupplier);
+ }
+ }
+
+ // Is a dummy factory requested?
+ else if (recyclerFactorySpec.equals("dummy")) {
+ return DummyRecyclerFactory.getInstance();
+ }
+
+ // Is a TLA factory requested?
+ else if (recyclerFactorySpec.equals("threadLocal")) {
+ return ThreadLocalRecyclerFactory.getInstance();
+ }
+
+ // Is a queueing factory requested?
+ else if (recyclerFactorySpec.startsWith("queue")) {
+ return readQueueingRecyclerFactory(recyclerFactorySpec, defaultCapacity);
+ }
+
+ // Bogus input, bail out.
+ else {
+ throw new IllegalArgumentException(
+ "invalid recycler factory: " + recyclerFactorySpec);
+ }
+
+ }
+
+ private static RecyclerFactory readQueueingRecyclerFactory(
+ final String recyclerFactorySpec,
+ final int defaultCapacity) {
+
+ // Parse the spec.
+ final String queueFactorySpec = recyclerFactorySpec.substring(
+ "queue".length() +
+ (recyclerFactorySpec.startsWith("queue:")
+ ? 1
+ : 0));
+ final Map<String, StringParameterParser.Value> parsedValues =
+ StringParameterParser.parse(
+ queueFactorySpec,
+ new LinkedHashSet<>(Arrays.asList("supplier", "capacity")));
+
+ // Read the supplier path.
+ final StringParameterParser.Value supplierValue = parsedValues.get("supplier");
+ final String supplierPath;
+ if (supplierValue == null || supplierValue instanceof StringParameterParser.NullValue) {
+ supplierPath = JCTOOLS_QUEUE_CLASS_AVAILABLE
+ ? JCTOOLS_QUEUE_CLASS_SUPPLIER_PATH
+ : "java.util.concurrent.ArrayBlockingQueue.new";
+ } else {
+ supplierPath = supplierValue.toString();
+ }
+
+ // Read the capacity.
+ final StringParameterParser.Value capacityValue = parsedValues.get("capacity");
+ final int capacity;
+ if (capacityValue == null || capacityValue instanceof StringParameterParser.NullValue) {
+ capacity = defaultCapacity;
+ } else {
+ try {
+ capacity = Integer.parseInt(capacityValue.toString());
+ } catch (final NumberFormatException error) {
+ throw new IllegalArgumentException(
+ "failed reading capacity in queueing recycler " +
+ "factory: " + queueFactorySpec, error);
+ }
+ }
+
+ // Execute the read spec.
+ return createRecyclerFactory(queueFactorySpec, supplierPath, capacity);
+
+ }
+
+ private static RecyclerFactory createRecyclerFactory(
+ final String queueFactorySpec,
+ final String supplierPath,
+ final int capacity) {
+ final int supplierPathSplitterIndex = supplierPath.lastIndexOf('.');
+ if (supplierPathSplitterIndex < 0) {
+ throw new IllegalArgumentException(
+ "invalid supplier in queueing recycler factory: " +
+ queueFactorySpec);
+ }
+ final String supplierClassName = supplierPath.substring(0, supplierPathSplitterIndex);
+ final String supplierMethodName = supplierPath.substring(supplierPathSplitterIndex + 1);
+ try {
+ final Class<?> supplierClass = LoaderUtil.loadClass(supplierClassName);
+ final Supplier<Queue<Object>> queueSupplier;
+ if ("new".equals(supplierMethodName)) {
+ final Constructor<?> supplierCtor =
+ supplierClass.getDeclaredConstructor(int.class);
+ queueSupplier = () -> {
+ try {
+ @SuppressWarnings("unchecked")
+ final Queue<Object> typedQueue =
+ (Queue<Object>) supplierCtor.newInstance(capacity);
+ return typedQueue;
+ } catch (final Exception error) {
+ throw new RuntimeException(
+ "recycler queue construction failed for factory: " +
+ queueFactorySpec, error);
+ }
+ };
+ } else {
+ final Method supplierMethod =
+ supplierClass.getMethod(supplierMethodName, int.class);
+ queueSupplier = () -> {
+ try {
+ @SuppressWarnings("unchecked")
+ final Queue<Object> typedQueue =
+ (Queue<Object>) supplierMethod.invoke(null, capacity);
+ return typedQueue;
+ } catch (final Exception error) {
+ throw new RuntimeException(
+ "recycler queue construction failed for factory: " +
+ queueFactorySpec, error);
+ }
+ };
+ }
+ return new QueueingRecyclerFactory(queueSupplier);
+ } catch (final Exception error) {
+ throw new RuntimeException(
+ "failed executing queueing recycler factory: " +
+ queueFactorySpec, error);
+ }
+ }
+
+}
diff --git a/log4j-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..4c03843
--- /dev/null
+++ b/log4j-layout-json-template/src/main/java/org/apache/logging/log4j/layout/json/template/util/Uris.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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}s 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 {
+ return unsafeReadUri(spec, charset);
+ } catch (final Exception error) {
+ final String message = String.format(
+ "failed reading URI (spec=%s, charset=%s)",
+ spec, charset);
+ throw new RuntimeException(message, error);
+ }
+ }
+
+ private static String unsafeReadUri(
+ final String spec,
+ final Charset charset)
+ throws Exception {
+ final URI uri = new URI(spec);
+ final String uriScheme = uri.getScheme().toLowerCase();
+ switch (uriScheme) {
+ case "classpath":
+ return readClassPathUri(uri, charset);
+ case "file":
+ return readFileUri(uri, charset);
+ default: {
+ final String message = String.format("unknown URI scheme (spec=%s)", spec);
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ }
+
+ 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..7004e54
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/EcsLayout.json
@@ -0,0 +1,12 @@
+{
+ "@timestamp": "${json:timestamp:pattern=yyyy-MM-dd'T'HH:mm:ss.SSS'Z',timeZone=UTC}",
+ "log.level": "${json:level}",
+ "message": "${json:message}",
+ "process.thread.name": "${json:thread:name}",
+ "log.logger": "${json:logger:name}",
+ "labels": "${json:mdc:flatten=labels.,stringify}",
+ "tags": "${json:ndc}",
+ "error.type": "${json:exception:className}",
+ "error.message": "${json:exception:message}",
+ "error.stack_trace": "${json:exception:stackTrace:text}"
+}
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..bcea9ee
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/GelfLayout.json
@@ -0,0 +1,11 @@
+{
+ "version": "1.1",
+ "host": "${hostName}",
+ "short_message": "${json:message}",
+ "full_message": "${json:exception:stackTrace:text}",
+ "timestamp": "${json:timestamp:epoch:secs}",
+ "level": "${json:level:severity:code}",
+ "_logger": "${json:logger:name}",
+ "_thread": "${json:thread:name}",
+ "_mdc": "${json:mdc:flatten=_,stringify}"
+}
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..f31ec63
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/JsonLayout.json
@@ -0,0 +1,26 @@
+{
+ "instant": {
+ "epochSecond": "${json:timestamp:epoch:secs,integral}",
+ "nanoOfSecond": "${json:timestamp:epoch:secs.nanos}"
+ },
+ "thread": "${json:thread:name}",
+ "level": "${json:level}",
+ "loggerName": "${json:logger:name}",
+ "message": "${json:message}",
+ "thrown": {
+ "message": "${json:exception:message}",
+ "name": "${json:exception:className}",
+ "extendedStackTrace": "${json:exception:stackTrace}"
+ },
+ "contextStack": "${json:ndc}",
+ "endOfBatch": "${json:endOfBatch}",
+ "loggerFqcn": "${json:logger:fqcn}",
+ "threadId": "${json:thread:id}",
+ "threadPriority": "${json:thread:priority}",
+ "source": {
+ "class": "${json:source:className}",
+ "method": "${json:source:methodName}",
+ "file": "${json:source:fileName}",
+ "line": "${json:source: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..62d4c20
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/LogstashJsonEventLayoutV1.json
@@ -0,0 +1,19 @@
+{
+ "mdc": "${json:mdc}",
+ "exception": {
+ "exception_class": "${json:exception:className}",
+ "exception_message": "${json:exception:message}",
+ "stacktrace": "${json:exception:stackTrace:text}"
+ },
+ "line_number": "${json:source:lineNumber}",
+ "class": "${json:source:className}",
+ "@version": 1,
+ "source_host": "${hostName}",
+ "message": "${json:message}",
+ "thread_name": "${json:thread:name}",
+ "@timestamp": "${json:timestamp}",
+ "level": "${json:level}",
+ "file": "${json:source:fileName}",
+ "method": "${json:source:methodName}",
+ "logger_name": "${json:logger: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..3faacd4
--- /dev/null
+++ b/log4j-layout-json-template/src/main/resources/StackTraceElementLayout.json
@@ -0,0 +1,6 @@
+{
+ "class": "${json:stackTraceElement:className}",
+ "method": "${json:stackTraceElement:methodName}",
+ "file": "${json:stackTraceElement:fileName}",
+ "line": "${json:stackTraceElement: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..ec96407
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/EcsLayoutTest.java
@@ -0,0 +1,80 @@
+package org.apache.logging.log4j.layout.json.template;
+
+import co.elastic.logging.log4j2.EcsLayout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.util.KeyValuePair;
+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 JsonTemplateLayout JSON_TEMPLATE_LAYOUT = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplateUri("classpath:EcsLayout.json")
+ .setEventTemplateAdditionalFields(
+ JsonTemplateLayout
+ .EventTemplateAdditionalFields
+ .newBuilder()
+ .setAdditionalFields(
+ new KeyValuePair[]{
+ new KeyValuePair(
+ "service.name",
+ "test")
+ })
+ .build())
+ .build();
+
+ private static final EcsLayout ECS_LAYOUT = EcsLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setServiceName("test")
+ .build();
+
+ @Test
+ public void test_lite_log_events() throws Exception {
+ final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
+ test(logEvents);
+ }
+
+ @Test
+ public void test_full_log_events() throws Exception {
+ final List<LogEvent> logEvents = LogEventFixture.createFullLogEvents(1_000);
+ test(logEvents);
+ }
+
+ private static void test(final Collection<LogEvent> logEvents) throws Exception {
+ for (final LogEvent logEvent : logEvents) {
+ test(logEvent);
+ }
+ }
+
+ private static void test(final LogEvent logEvent) throws Exception {
+ 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)
+ throws Exception {
+ return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
+ }
+
+ private static Map<String, Object> renderUsingEcsLayout(
+ final LogEvent logEvent)
+ throws Exception {
+ 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..b561ea6
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/GelfLayoutTest.java
@@ -0,0 +1,102 @@
+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.util.KeyValuePair;
+import org.assertj.core.api.Assertions;
+import org.assertj.core.data.Percentage;
+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 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 KeyValuePair[]{
+ new KeyValuePair(
+ "host",
+ HOST_NAME)
+ })
+ .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() throws Exception {
+ final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
+ test(logEvents);
+ }
+
+ @Test
+ public void test_full_log_events() throws Exception {
+ final List<LogEvent> logEvents = LogEventFixture.createFullLogEvents(1_000);
+ test(logEvents);
+ }
+
+ private static void test(final Collection<LogEvent> logEvents) throws Exception {
+ for (final LogEvent logEvent : logEvents) {
+ test(logEvent);
+ }
+ }
+
+ private static void test(final LogEvent logEvent) throws Exception {
+ final Map<String, Object> jsonTemplateLayoutMap = renderUsingJsonTemplateLayout(logEvent);
+ final Map<String, Object> gelfLayoutMap = renderUsingGelfLayout(logEvent);
+ verifyTimestamp(jsonTemplateLayoutMap, gelfLayoutMap);
+ Assertions.assertThat(jsonTemplateLayoutMap).isEqualTo(gelfLayoutMap);
+ }
+
+ private static Map<String, Object> renderUsingJsonTemplateLayout(
+ final LogEvent logEvent)
+ throws Exception {
+ return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
+ }
+
+ private static Map<String, Object> renderUsingGelfLayout(
+ final LogEvent logEvent)
+ throws Exception {
+ return renderUsing(logEvent, GELF_LAYOUT);
+ }
+
+ /**
+ * Handle timestamps individually to avoid floating-point comparison hiccups.
+ */
+ private static void verifyTimestamp(
+ final Map<String, Object> jsonTemplateLayoutMap,
+ final Map<String, Object> gelfLayoutMap) {
+ final Number jsonTemplateLayoutTimestamp =
+ (Number) jsonTemplateLayoutMap.remove("timestamp");
+ final Number gelfLayoutTimestamp =
+ (Number) gelfLayoutMap.remove("timestamp");
+ Assertions
+ .assertThat(jsonTemplateLayoutTimestamp.doubleValue())
+ .isCloseTo(
+ gelfLayoutTimestamp.doubleValue(),
+ Percentage.withPercentage(0.01));
+ }
+
+}
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..0eb077f
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JacksonFixture.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 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.core.JsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public enum JacksonFixture {;
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ private static final JsonFactory JSON_FACTORY = OBJECT_MAPPER.getFactory();
+
+ public static ObjectMapper getObjectMapper() {
+ return OBJECT_MAPPER;
+ }
+
+ public static JsonFactory getJsonFactory() {
+ return JSON_FACTORY;
+ }
+
+}
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..7bbd9a9
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonLayoutTest.java
@@ -0,0 +1,102 @@
+package org.apache.logging.log4j.layout.json.template;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.jackson.json.layout.JsonLayout;
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static org.apache.logging.log4j.layout.json.template.LayoutComparisonHelpers.renderUsing;
+
+public class JsonLayoutTest {
+
+ private static final Configuration CONFIGURATION = new DefaultConfiguration();
+
+ private static final JsonTemplateLayout JSON_TEMPLATE_LAYOUT = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplateUri("classpath:JsonLayout.json")
+ .build();
+
+ private static final JsonLayout JSON_LAYOUT = JsonLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .build();
+
+ @Test
+ public void test_lite_log_events() throws Exception {
+ final List<LogEvent> logEvents = LogEventFixture.createLiteLogEvents(1_000);
+ test(logEvents);
+ }
+
+ @Test
+ public void test_full_log_events() throws Exception {
+ final List<LogEvent> logEvents = LogEventFixture.createFullLogEvents(1_000);
+ test(logEvents);
+ }
+
+ private static void test(final Collection<LogEvent> logEvents) throws Exception {
+ for (final LogEvent logEvent : logEvents) {
+ test(logEvent);
+ }
+ }
+
+ private static void test(final LogEvent logEvent) throws Exception {
+ final Map<String, Object> jsonTemplateLayoutMap = renderUsingJsonTemplateLayout(logEvent);
+ final Map<String, Object> jsonLayoutMap = renderUsingJsonLayout(logEvent);
+ Assertions.assertThat(jsonTemplateLayoutMap).isEqualTo(jsonLayoutMap);
+ }
+
+ private static Map<String, Object> renderUsingJsonTemplateLayout(
+ final LogEvent logEvent)
+ throws Exception {
+ final Map<String, Object> map = renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
+ final Map<String, Object> emptySourceExcludedMap = removeEmptyObject(map, "source");
+ // JsonLayout blindly serializes the Throwable as a POJO, this is,
+ // to say the least, quite wrong, and I ain't gonna try to emulate
+ // this behaviour in JsonTemplateLayout. Hence, discarding the "thrown"
+ // field.
+ emptySourceExcludedMap.remove("thrown");
+ return emptySourceExcludedMap;
+ }
+
+ private static Map<String, Object> renderUsingJsonLayout(
+ final LogEvent logEvent)
+ throws Exception {
+ final Map<String, Object> map = renderUsing(logEvent, JSON_LAYOUT);
+ // JsonLayout blindly serializes the Throwable as a POJO, this is,
+ // to say the least, quite wrong, and I ain't gonna try to emulate
+ // this behaviour in JsonTemplateLayout. Hence, discarding the "thrown"
+ // field.
+ map.remove("thrown");
+ return map;
+ }
+
+ private static Map<String, Object> removeEmptyObject(
+ final Map<String, Object> root,
+ final String key) {
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> source =
+ (Map<String, Object>) root.getOrDefault(
+ key, Collections.emptyMap());
+ boolean emptySource = source
+ .values()
+ .stream()
+ .allMatch(Objects::isNull);
+ if (!emptySource) {
+ return root;
+ }
+ final Map<String, Object> trimmedRoot = new LinkedHashMap<>(root);
+ trimmedRoot.remove(key);
+ return trimmedRoot;
+ }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutConcurrentEncodeTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutConcurrentEncodeTest.java
new file mode 100644
index 0000000..d17a8be
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutConcurrentEncodeTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.json.template;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.layout.ByteBufferDestination;
+import org.assertj.core.api.Assertions;
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+public class JsonTemplateLayoutConcurrentEncodeTest {
+
+ private static class ConcurrentAccessError extends RuntimeException {
+
+ public static final long serialVersionUID = 0;
+
+ private ConcurrentAccessError(final int concurrentAccessCount) {
+ super("concurrentAccessCount=" + concurrentAccessCount);
+ }
+
+ }
+
+ private static class ConcurrentAccessDetectingByteBufferDestination
+ extends BlackHoleByteBufferDestination {
+
+ private final AtomicInteger concurrentAccessCounter = new AtomicInteger(0);
+
+ ConcurrentAccessDetectingByteBufferDestination() {
+ super(2_000);
+ }
+
+ @Override
+ public ByteBuffer getByteBuffer() {
+ final int concurrentAccessCount = concurrentAccessCounter.incrementAndGet();
+ if (concurrentAccessCount > 1) {
+ throw new ConcurrentAccessError(concurrentAccessCount);
+ }
+ try {
+ return super.getByteBuffer();
+ } finally {
+ concurrentAccessCounter.decrementAndGet();
+ }
+ }
+
+ @Override
+ public ByteBuffer drain(final ByteBuffer byteBuffer) {
+ final int concurrentAccessCount = concurrentAccessCounter.incrementAndGet();
+ if (concurrentAccessCount > 1) {
+ throw new ConcurrentAccessError(concurrentAccessCount);
+ }
+ try {
+ return super.drain(byteBuffer);
+ } finally {
+ concurrentAccessCounter.decrementAndGet();
+ }
+ }
+
+ @Override
+ public void writeBytes(final ByteBuffer byteBuffer) {
+ final int concurrentAccessCount = concurrentAccessCounter.incrementAndGet();
+ if (concurrentAccessCount > 1) {
+ throw new ConcurrentAccessError(concurrentAccessCount);
+ }
+ try {
+ super.writeBytes(byteBuffer);
+ } finally {
+ concurrentAccessCounter.decrementAndGet();
+ }
+ }
+
+ @Override
+ public void writeBytes(final byte[] buffer, final int offset, final int length) {
+ int concurrentAccessCount = concurrentAccessCounter.incrementAndGet();
+ if (concurrentAccessCount > 1) {
+ throw new ConcurrentAccessError(concurrentAccessCount);
+ }
+ try {
+ super.writeBytes(buffer, offset, length);
+ } finally {
+ concurrentAccessCounter.decrementAndGet();
+ }
+ }
+
+ }
+
+ private static final LogEvent[] LOG_EVENTS = createMessages();
+
+ private static LogEvent[] createMessages() {
+ final int messageCount = 1_000;
+ final LogEvent[] logEvents = new LogEvent[messageCount];
+ LogEventFixture
+ .createLiteLogEvents(messageCount)
+ .toArray(logEvents);
+ return logEvents;
+ }
+
+ @Test
+ public void test_concurrent_encode() {
+ final AtomicReference<Exception> encodeFailureRef = new AtomicReference<>(null);
+ produce(encodeFailureRef);
+ Assertions.assertThat(encodeFailureRef.get()).isNull();
+ }
+
+ private void produce(final AtomicReference<Exception> encodeFailureRef) {
+ final int threadCount = 10;
+ final JsonTemplateLayout layout = createLayout();
+ final ByteBufferDestination destination =
+ new ConcurrentAccessDetectingByteBufferDestination();
+ final AtomicLong encodeCounter = new AtomicLong(0);
+ final List<Thread> workers = IntStream
+ .range(0, threadCount)
+ .mapToObj((final int threadIndex) ->
+ createWorker(
+ layout,
+ destination,
+ encodeFailureRef,
+ encodeCounter,
+ threadIndex))
+ .collect(Collectors.toList());
+ workers.forEach(Thread::start);
+ workers.forEach((final Thread worker) -> {
+ try {
+ worker.join();
+ } catch (final InterruptedException ignored) {
+ System.err.format("join to %s interrupted%n", worker.getName());
+ }
+ });
+ }
+
+ private static JsonTemplateLayout createLayout() {
+ final Configuration config = new DefaultConfiguration();
+ return JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(config)
+ .setEventTemplate("{\"message\": \"${json:message}\"}")
+ .setStackTraceEnabled(false)
+ .setLocationInfoEnabled(false)
+ .build();
+ }
+
+ private Thread createWorker(
+ final JsonTemplateLayout layout,
+ final ByteBufferDestination destination,
+ final AtomicReference<Exception> encodeFailureRef,
+ final AtomicLong encodeCounter,
+ final int threadIndex) {
+ final int maxEncodeCount = 1_000;
+ final String threadName = String.format("Worker-%d", threadIndex);
+ return new Thread(
+ () -> {
+ try {
+ for (int logEventIndex = threadIndex % LOG_EVENTS.length;
+ encodeFailureRef.get() == null && encodeCounter.incrementAndGet() < maxEncodeCount;
+ logEventIndex = (logEventIndex + 1) % LOG_EVENTS.length) {
+ final LogEvent logEvent = LOG_EVENTS[logEventIndex];
+ layout.encode(logEvent, destination);
+ }
+ } catch (final Exception error) {
+ final boolean succeeded = encodeFailureRef.compareAndSet(null, error);
+ if (succeeded) {
+ System.err.format("%s failed%n", threadName);
+ error.printStackTrace(System.err);
+ }
+ }
+ },
+ threadName);
+ }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutGcFreeTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutGcFreeTest.java
new file mode 100644
index 0000000..dfe097a
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutGcFreeTest.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.json.template;
+
+import org.apache.logging.log4j.core.GcFreeLoggingTestUtil;
+import org.junit.Test;
+
+public class JsonTemplateLayoutGcFreeTest {
+
+ @Test
+ public void test_no_allocation_during_steady_state_logging() throws Exception {
+ GcFreeLoggingTestUtil.runTest(getClass());
+ }
+
+ /**
+ * This code runs in a separate process, instrumented with the Google Allocation Instrumenter.
+ */
+ public static void main(final String[] args) throws Exception {
+ System.setProperty("log4j.layout.jsonTemplate.recyclerFactory", "threadLocal");
+ System.setProperty("log4j2.garbagefree.threadContextMap", "true");
+ GcFreeLoggingTestUtil.executeLogging(
+ "gcFreeJsonTemplateLayoutLogging.xml",
+ JsonTemplateLayoutGcFreeTest.class);
+ }
+
+}
diff --git a/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java
new file mode 100644
index 0000000..4e330f9
--- /dev/null
+++ b/log4j-layout-json-template/src/test/java/org/apache/logging/log4j/layout/json/template/JsonTemplateLayoutTest.java
@@ -0,0 +1,1653 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+package org.apache.logging.log4j.layout.json.template;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.MappingIterator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.MissingNode;
+import com.fasterxml.jackson.databind.node.NullNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.MarkerManager;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.SocketAppender;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.core.layout.ByteBufferDestination;
+import org.apache.logging.log4j.core.lookup.MainMapLookup;
+import org.apache.logging.log4j.core.net.Severity;
+import org.apache.logging.log4j.core.time.MutableInstant;
+import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.message.ObjectMessage;
+import org.apache.logging.log4j.message.SimpleMessage;
+import org.apache.logging.log4j.message.StringMapMessage;
+import org.apache.logging.log4j.test.AvailablePortFinder;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
+import org.apache.logging.log4j.util.StringMap;
+import org.apache.logging.log4j.util.Strings;
+import org.assertj.core.data.Percentage;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SuppressWarnings("DoubleBraceInitialization")
+public class JsonTemplateLayoutTest {
+
+ private static final Configuration CONFIGURATION = new DefaultConfiguration();
+
+ private static final List<LogEvent> LOG_EVENTS = LogEventFixture.createFullLogEvents(5);
+
+ private static final JsonNodeFactory JSON_NODE_FACTORY = JsonNodeFactory.instance;
+
+ private static final ObjectMapper OBJECT_MAPPER = JacksonFixture.getObjectMapper();
+
+ private static final String LOGGER_NAME = JsonTemplateLayoutTest.class.getSimpleName();
+
+ @Test
+ public void test_serialized_event() throws IOException {
+ final String lookupTestKey = "lookup_test_key";
+ final String lookupTestVal =
+ String.format("lookup_test_value_%d", (int) (1000 * Math.random()));
+ System.setProperty(lookupTestKey, lookupTestVal);
+ for (final LogEvent logEvent : LOG_EVENTS) {
+ checkLogEvent(logEvent, lookupTestKey, lookupTestVal);
+ }
+ }
+
+ private void checkLogEvent(
+ final LogEvent logEvent,
+ @SuppressWarnings("SameParameterValue")
+ final String lookupTestKey,
+ final String lookupTestVal) throws IOException {
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplateUri("classpath:testJsonTemplateLayout.json")
+ .setStackTraceEnabled(true)
+ .setLocationInfoEnabled(true)
+ .build();
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readValue(serializedLogEvent, JsonNode.class);
+ checkConstants(rootNode);
+ checkBasicFields(logEvent, rootNode);
+ checkSource(logEvent, rootNode);
+ checkException(layout.getCharset(), logEvent, rootNode);
+ checkLookupTest(lookupTestKey, lookupTestVal, rootNode);
+ }
+
+ private static void checkConstants(final JsonNode rootNode) {
+ assertThat(point(rootNode, "@version").asInt()).isEqualTo(1);
+ }
+
+ private static void checkBasicFields(final LogEvent logEvent, final JsonNode rootNode) {
+ assertThat(point(rootNode, "message").asText())
+ .isEqualTo(logEvent.getMessage().getFormattedMessage());
+ assertThat(point(rootNode, "level").asText())
+ .isEqualTo(logEvent.getLevel().name());
+ assertThat(point(rootNode, "logger_fqcn").asText())
+ .isEqualTo(logEvent.getLoggerFqcn());
+ assertThat(point(rootNode, "logger_name").asText())
+ .isEqualTo(logEvent.getLoggerName());
+ assertThat(point(rootNode, "thread_id").asLong())
+ .isEqualTo(logEvent.getThreadId());
+ assertThat(point(rootNode, "thread_name").asText())
+ .isEqualTo(logEvent.getThreadName());
+ assertThat(point(rootNode, "thread_priority").asInt())
+ .isEqualTo(logEvent.getThreadPriority());
+ assertThat(point(rootNode, "end_of_batch").asBoolean())
+ .isEqualTo(logEvent.isEndOfBatch());
+ }
+
+ private static void checkSource(final LogEvent logEvent, final JsonNode rootNode) {
+ assertThat(point(rootNode, "class").asText()).isEqualTo(logEvent.getSource().getClassName());
+ assertThat(point(rootNode, "file").asText()).isEqualTo(logEvent.getSource().getFileName());
+ assertThat(point(rootNode, "line_number").asInt()).isEqualTo(logEvent.getSource().getLineNumber());
+ }
+
+ private static void checkException(
+ final Charset charset,
+ final LogEvent logEvent,
+ final JsonNode rootNode) {
+ final Throwable thrown = logEvent.getThrown();
+ if (thrown != null) {
+ assertThat(point(rootNode, "exception_class").asText()).isEqualTo(thrown.getClass().getCanonicalName());
+ assertThat(point(rootNode, "exception_message").asText()).isEqualTo(thrown.getMessage());
+ final String stackTrace = serializeStackTrace(charset, thrown);
+ assertThat(point(rootNode, "stacktrace").asText()).isEqualTo(stackTrace);
+ }
+ }
+
+ private static String serializeStackTrace(
+ final Charset charset,
+ final Throwable exception) {
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ final String charsetName = charset.name();
+ try (final PrintStream printStream =
+ new PrintStream(outputStream, false, charsetName)) {
+ exception.printStackTrace(printStream);
+ return outputStream.toString(charsetName);
+ } catch (final UnsupportedEncodingException error) {
+ throw new RuntimeException("failed converting the stack trace to string", error);
+ }
+ }
+
+ private static void checkLookupTest(
+ final String lookupTestKey,
+ final String lookupTestVal,
+ final JsonNode rootNode) {
+ assertThat(point(rootNode, lookupTestKey).asText()).isEqualTo(lookupTestVal);
+ }
+
+ private static JsonNode point(final JsonNode node, final Object... fields) {
+ final String pointer = createJsonPointer(fields);
+ return node.at(pointer);
+ }
+
+ private static String createJsonPointer(final Object... fields) {
+ final StringBuilder jsonPathBuilder = new StringBuilder();
+ for (final Object field : fields) {
+ jsonPathBuilder.append("/").append(field);
+ }
+ return jsonPathBuilder.toString();
+ }
+
+ @Test
+ public void test_inline_template() throws Exception {
+
+ // Create the log event.
+ final SimpleMessage message = new SimpleMessage("Hello, World");
+ final String timestamp = "2017-09-28T17:13:29.098+02:00";
+ final long timeMillis = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
+ .parse(timestamp)
+ .getTime();
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.INFO)
+ .setMessage(message)
+ .setTimeMillis(timeMillis)
+ .build();
+
+ // Create the event template.
+ final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+ eventTemplateRootNode.put("@timestamp", "${json:timestamp:timeZone=Europe/Amsterdam}");
+ final String staticFieldName = "staticFieldName";
+ final String staticFieldValue = "staticFieldValue";
+ eventTemplateRootNode.put(staticFieldName, staticFieldValue);
+ final String eventTemplate = eventTemplateRootNode.toString();
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Check the serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
+ assertThat(point(rootNode, "@timestamp").asText()).isEqualTo(timestamp);
+ assertThat(point(rootNode, staticFieldName).asText()).isEqualTo(staticFieldValue);
+
+ }
+
+ @Test
+ public void test_log4j_deferred_runtime_resolver_for_MapMessage() throws Exception {
+
+ // Create the event template.
+ final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+ eventTemplateRootNode.put("mapValue3", "${json:message:json}");
+ eventTemplateRootNode.put("mapValue1", "${map:key1}");
+ eventTemplateRootNode.put("mapValue2", "${map:key2}");
+ eventTemplateRootNode.put(
+ "nestedLookupEmptyValue",
+ "${map:noExist:-${map:noExist2:-${map:noExist3:-}}}");
+ eventTemplateRootNode.put(
+ "nestedLookupStaticValue",
+ "${map:noExist:-${map:noExist2:-${map:noExist3:-Static Value}}}");
+ final String eventTemplate = eventTemplateRootNode.toString();
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Create the log event with a MapMessage.
+ final StringMapMessage mapMessage = new StringMapMessage()
+ .with("key1", "val1")
+ .with("key2", "val2")
+ .with("key3", Collections.singletonMap("foo", "bar"));
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.INFO)
+ .setMessage(mapMessage)
+ .setTimeMillis(System.currentTimeMillis())
+ .build();
+
+ // Check the serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
+ assertThat(point(rootNode, "mapValue1").asText()).isEqualTo("val1");
+ assertThat(point(rootNode, "mapValue2").asText()).isEqualTo("val2");
+ assertThat(point(rootNode, "nestedLookupEmptyValue").asText()).isEmpty();
+ assertThat(point(rootNode, "nestedLookupStaticValue").asText()).isEqualTo("Static Value");
+
+ }
+
+ @Test
+ public void test_MapMessage_serialization() throws Exception {
+
+ // Create the event template.
+ final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+ eventTemplateRootNode.put("message", "${json:message:json}");
+ final String eventTemplate = eventTemplateRootNode.toString();
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Create the log event with a MapMessage.
+ final StringMapMessage mapMessage = new StringMapMessage()
+ .with("key1", "val1")
+ .with("key2", 0xDEADBEEF)
+ .with("key3", Collections.singletonMap("key3.1", "val3.1"));
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.INFO)
+ .setMessage(mapMessage)
+ .setTimeMillis(System.currentTimeMillis())
+ .build();
+
+ // Check the serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
+ assertThat(point(rootNode, "message", "key1").asText()).isEqualTo("val1");
+ assertThat(point(rootNode, "message", "key2").asLong()).isEqualTo(0xDEADBEEF);
+ assertThat(point(rootNode, "message", "key3", "key3.1").asText()).isEqualTo("val3.1");
+
+ }
+
+ @Test
+ public void test_property_injection() throws Exception {
+
+ // Create the log event.
+ final SimpleMessage message = new SimpleMessage("Hello, World");
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.INFO)
+ .setMessage(message)
+ .build();
+
+ // Create the event template with property.
+ final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+ final String propertyName = "propertyName";
+ eventTemplateRootNode.put(propertyName, "${" + propertyName + "}");
+ final String eventTemplate = eventTemplateRootNode.toString();
+
+ // Create the layout with property.
+ final String propertyValue = "propertyValue";
+ final Configuration config = ConfigurationBuilderFactory
+ .newConfigurationBuilder()
+ .addProperty(propertyName, propertyValue)
+ .build();
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(config)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Check the serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
+ assertThat(point(rootNode, propertyName).asText()).isEqualTo(propertyValue);
+
+ }
+
+ @Test
+ public void test_empty_root_cause() throws Exception {
+
+ // Create the log event.
+ final SimpleMessage message = new SimpleMessage("Hello, World!");
+ final RuntimeException exception = new RuntimeException("failure for test purposes");
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.ERROR)
+ .setMessage(message)
+ .setThrown(exception)
+ .build();
+
+ // Create the event template.
+ final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+ eventTemplateRootNode.put("ex_class", "${json:exception:className}");
+ eventTemplateRootNode.put("ex_message", "${json:exception:message}");
+ eventTemplateRootNode.put("ex_stacktrace", "${json:exception:stackTrace:text}");
+ eventTemplateRootNode.put("root_ex_class", "${json:exceptionRootCause:className}");
+ eventTemplateRootNode.put("root_ex_message", "${json:exceptionRootCause:message}");
+ eventTemplateRootNode.put("root_ex_stacktrace", "${json:exceptionRootCause:stackTrace:text}");
+ final String eventTemplate = eventTemplateRootNode.toString();
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setStackTraceEnabled(true)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Check the serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
+ assertThat(point(rootNode, "ex_class").asText())
+ .isEqualTo(exception.getClass().getCanonicalName());
+ assertThat(point(rootNode, "ex_message").asText())
+ .isEqualTo(exception.getMessage());
+ assertThat(point(rootNode, "ex_stacktrace").asText())
+ .startsWith(exception.getClass().getCanonicalName() + ": " + exception.getMessage());
+ assertThat(point(rootNode, "root_ex_class").asText())
+ .isEqualTo(point(rootNode, "ex_class").asText());
+ assertThat(point(rootNode, "root_ex_message").asText())
+ .isEqualTo(point(rootNode, "ex_message").asText());
+ assertThat(point(rootNode, "root_ex_stacktrace").asText())
+ .isEqualTo(point(rootNode, "ex_stacktrace").asText());
+
+ }
+
+ @Test
+ public void test_root_cause() throws Exception {
+
+ // Create the log event.
+ final SimpleMessage message = new SimpleMessage("Hello, World!");
+ final RuntimeException exceptionCause = new RuntimeException("failure cause for test purposes");
+ final RuntimeException exception = new RuntimeException("failure for test purposes", exceptionCause);
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.ERROR)
+ .setMessage(message)
+ .setThrown(exception)
+ .build();
+
+ // Create the event template.
+ final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+ eventTemplateRootNode.put("ex_class", "${json:exception:className}");
+ eventTemplateRootNode.put("ex_message", "${json:exception:message}");
+ eventTemplateRootNode.put("ex_stacktrace", "${json:exception:stackTrace:text}");
+ eventTemplateRootNode.put("root_ex_class", "${json:exceptionRootCause:className}");
+ eventTemplateRootNode.put("root_ex_message", "${json:exceptionRootCause:message}");
+ eventTemplateRootNode.put("root_ex_stacktrace", "${json:exceptionRootCause:stackTrace:text}");
+ final String eventTemplate = eventTemplateRootNode.toString();
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setStackTraceEnabled(true)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Check the serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
+ assertThat(point(rootNode, "ex_class").asText())
+ .isEqualTo(exception.getClass().getCanonicalName());
+ assertThat(point(rootNode, "ex_message").asText())
+ .isEqualTo(exception.getMessage());
+ assertThat(point(rootNode, "ex_stacktrace").asText())
+ .startsWith(exception.getClass().getCanonicalName() + ": " + exception.getMessage());
+ assertThat(point(rootNode, "root_ex_class").asText())
+ .isEqualTo(exceptionCause.getClass().getCanonicalName());
+ assertThat(point(rootNode, "root_ex_message").asText())
+ .isEqualTo(exceptionCause.getMessage());
+ assertThat(point(rootNode, "root_ex_stacktrace").asText())
+ .startsWith(exceptionCause.getClass().getCanonicalName() + ": " + exceptionCause.getMessage());
+
+ }
+
+ @Test
+ public void test_marker_name() throws IOException {
+
+ // Create the log event.
+ final SimpleMessage message = new SimpleMessage("Hello, World!");
+ final String markerName = "test";
+ final Marker marker = MarkerManager.getMarker(markerName);
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.ERROR)
+ .setMessage(message)
+ .setMarker(marker)
+ .build();
+
+ // Create the event template.
+ final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+ final String messageKey = "message";
+ eventTemplateRootNode.put(messageKey, "${json:message}");
+ final String markerNameKey = "marker";
+ eventTemplateRootNode.put(markerNameKey, "${json:marker:name}");
+ final String eventTemplate = eventTemplateRootNode.toString();
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Check the serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
+ assertThat(point(rootNode, messageKey).asText()).isEqualTo(message.getFormattedMessage());
+ assertThat(point(rootNode, markerNameKey).asText()).isEqualTo(markerName);
+
+ }
+
+ @Test
+ public void test_lineSeparator_suffix() {
+
+ // Create the log event.
+ final SimpleMessage message = new SimpleMessage("Hello, World!");
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.INFO)
+ .setMessage(message)
+ .build();
+
+ // Check line separators.
+ test_lineSeparator_suffix(logEvent, true);
+ test_lineSeparator_suffix(logEvent, false);
+
+ }
+
+ private void test_lineSeparator_suffix(
+ final LogEvent logEvent,
+ final boolean prettyPrintEnabled) {
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplateUri("classpath:LogstashJsonEventLayoutV1.json")
+ .build();
+
+ // Check the serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final String assertionCaption = String.format("testing lineSeperator (prettyPrintEnabled=%s)", prettyPrintEnabled);
+ assertThat(serializedLogEvent).as(assertionCaption).endsWith("}" + System.lineSeparator());
+
+ }
+
+ @Test
+ public void test_main_key_access() throws IOException {
+
+ // Set main() arguments.
+ final String kwKey = "--name";
+ final String kwVal = "aNameValue";
+ final String positionArg = "position2Value";
+ final String missingKwKey = "--missing";
+ final String[] mainArgs = {kwKey, kwVal, positionArg};
+ MainMapLookup.setMainArguments(mainArgs);
+
+ // Create the log event.
+ final SimpleMessage message = new SimpleMessage("Hello, World!");
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.INFO)
+ .setMessage(message)
+ .build();
+
+ // Create the template.
+ final ObjectNode templateRootNode = JSON_NODE_FACTORY.objectNode();
+ templateRootNode.put("name", String.format("${json:main:%s}", kwKey));
+ templateRootNode.put("positionArg", "${json:main:2}");
+ templateRootNode.put("notFoundArg", String.format("${json:main:%s}", missingKwKey));
+ final String template = templateRootNode.toString();
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplate(template)
+ .build();
+
+ // Check the serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
+ assertThat(point(rootNode, "name").asText()).isEqualTo(kwVal);
+ assertThat(point(rootNode, "positionArg").asText()).isEqualTo(positionArg);
+ assertThat(point(rootNode, "notFoundArg")).isInstanceOf(NullNode.class);
+
+ }
+
+ @Test
+ public void test_mdc_key_access() throws IOException {
+
+ // Create the log event.
+ final SimpleMessage message = new SimpleMessage("Hello, World!");
+ final StringMap contextData = new SortedArrayStringMap();
+ final String mdcDirectlyAccessedKey = "mdcKey1";
+ final String mdcDirectlyAccessedValue = "mdcValue1";
+ contextData.putValue(mdcDirectlyAccessedKey, mdcDirectlyAccessedValue);
+ final String mdcDirectlyAccessedNullPropertyKey = "mdcKey2";
+ final String mdcDirectlyAccessedNullPropertyValue = null;
+ // noinspection ConstantConditions
+ contextData.putValue(mdcDirectlyAccessedNullPropertyKey, mdcDirectlyAccessedNullPropertyValue);
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.INFO)
+ .setMessage(message)
+ .setContextData(contextData)
+ .build();
+
+ // Create the event template.
+ final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+ eventTemplateRootNode.put(
+ mdcDirectlyAccessedKey,
+ String.format("${json:mdc:key=%s}", mdcDirectlyAccessedKey));
+ eventTemplateRootNode.put(
+ mdcDirectlyAccessedNullPropertyKey,
+ String.format("${json:mdc:key=%s}", mdcDirectlyAccessedNullPropertyKey));
+ String eventTemplate = eventTemplateRootNode.toString();
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setStackTraceEnabled(true)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Check the serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
+ assertThat(point(rootNode, mdcDirectlyAccessedKey).asText()).isEqualTo(mdcDirectlyAccessedValue);
+ assertThat(point(rootNode, mdcDirectlyAccessedNullPropertyKey)).isInstanceOf(NullNode.class);
+
+ }
+
+ @Test
+ public void test_mdc_pattern() throws IOException {
+
+ // Create the log event.
+ final SimpleMessage message = new SimpleMessage("Hello, World!");
+ final StringMap contextData = new SortedArrayStringMap();
+ final String mdcPatternMatchedKey = "mdcKey1";
+ final String mdcPatternMatchedValue = "mdcValue1";
+ contextData.putValue(mdcPatternMatchedKey, mdcPatternMatchedValue);
+ final String mdcPatternMismatchedKey = "mdcKey2";
+ final String mdcPatternMismatchedValue = "mdcValue2";
+ contextData.putValue(mdcPatternMismatchedKey, mdcPatternMismatchedValue);
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.INFO)
+ .setMessage(message)
+ .setContextData(contextData)
+ .build();
+
+ // Create the event template.
+ final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+ final String mdcFieldName = "mdc";
+ eventTemplateRootNode.put(mdcFieldName, "${json:mdc:pattern=" + mdcPatternMatchedKey + "}");
+ String eventTemplate = eventTemplateRootNode.toString();
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setStackTraceEnabled(true)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Check the serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
+ assertThat(point(rootNode, mdcFieldName, mdcPatternMatchedKey).asText()).isEqualTo(mdcPatternMatchedValue);
+ assertThat(point(rootNode, mdcFieldName, mdcPatternMismatchedKey)).isInstanceOf(MissingNode.class);
+
+ }
+
+ @Test
+ public void test_mdc_flatten() throws IOException {
+
+ // Create the log event.
+ final SimpleMessage message = new SimpleMessage("Hello, World!");
+ final StringMap contextData = new SortedArrayStringMap();
+ final String mdcPatternMatchedKey = "mdcKey1";
+ final String mdcPatternMatchedValue = "mdcValue1";
+ contextData.putValue(mdcPatternMatchedKey, mdcPatternMatchedValue);
+ final String mdcPatternMismatchedKey = "mdcKey2";
+ final String mdcPatternMismatchedValue = "mdcValue2";
+ contextData.putValue(mdcPatternMismatchedKey, mdcPatternMismatchedValue);
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.INFO)
+ .setMessage(message)
+ .setContextData(contextData)
+ .build();
+
+ // Create the event template.
+ final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+ final String mdcPrefix = "_mdc.";
+ eventTemplateRootNode.put(
+ mdcPrefix,
+ "${json:mdc:flatten=" + mdcPrefix + ",pattern=" + mdcPatternMatchedKey + "}");
+ String eventTemplate = eventTemplateRootNode.toString();
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setStackTraceEnabled(true)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Check the serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
+ assertThat(point(rootNode, mdcPrefix + mdcPatternMatchedKey).asText()).isEqualTo(mdcPatternMatchedValue);
+ assertThat(point(rootNode, mdcPrefix + mdcPatternMismatchedKey)).isInstanceOf(MissingNode.class);
+
+ }
+
+ @Test
+ public void test_MapResolver() throws IOException {
+
+ // Create the log event.
+ final StringMapMessage message = new StringMapMessage().with("key1", "val1");
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.INFO)
+ .setMessage(message)
+ .build();
+
+ // Create the event template node with map values.
+ final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+ eventTemplateRootNode.put("mapValue1", "${json:map:key1}");
+ eventTemplateRootNode.put("mapValue2", "${json:map:noExist}");
+ final String eventTemplate = eventTemplateRootNode.toString();
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Check serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
+ assertThat(point(rootNode, "mapValue1").asText()).isEqualTo("val1");
+ assertThat(point(rootNode, "mapValue2")).isInstanceOf(NullNode.class);
+
+ }
+
+ @Test
+ public void test_message_json() throws IOException {
+
+ // Create the log event.
+ final StringMapMessage message = new StringMapMessage();
+ message.put("message", "Hello, World!");
+ message.put("bottle", "Kickapoo Joy Juice");
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.INFO)
+ .setMessage(message)
+ .build();
+
+ // Create the event template.
+ final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+ eventTemplateRootNode.put("message", "${json:message:json}");
+ final String eventTemplate = eventTemplateRootNode.toString();
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setStackTraceEnabled(true)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Check the serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
+ assertThat(point(rootNode, "message", "message").asText()).isEqualTo("Hello, World!");
+ assertThat(point(rootNode, "message", "bottle").asText()).isEqualTo("Kickapoo Joy Juice");
+
+ }
+
+ @Test
+ public void test_message_json_fallback() throws IOException {
+
+ // Create the log event.
+ final SimpleMessage message = new SimpleMessage("Hello, World!");
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setLoggerName(LOGGER_NAME)
+ .setLevel(Level.INFO)
+ .setMessage(message)
+ .build();
+
+ // Create the event template.
+ final ObjectNode eventTemplateRootNode = JSON_NODE_FACTORY.objectNode();
+ eventTemplateRootNode.put("message", "${json:message:json}");
+ final String eventTemplate = eventTemplateRootNode.toString();
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setStackTraceEnabled(true)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Check the serialized event.
+ final String serializedLogEvent = layout.toSerializable(logEvent);
+ final JsonNode rootNode = OBJECT_MAPPER.readTree(serializedLogEvent);
+ assertThat(point(rootNode, "message").asText()).isEqualTo("Hello, World!");
+
... 9055 lines suppressed ...