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 2021/05/10 10:32:52 UTC
[logging-log4j2] branch release-2.x updated: LOG4J2-3051 Add
CaseConverterResolver to JsonTemplateLayout (#490)
This is an automated email from the ASF dual-hosted git repository.
vy pushed a commit to branch release-2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/release-2.x by this push:
new 1dbe6e7 LOG4J2-3051 Add CaseConverterResolver to JsonTemplateLayout (#490)
1dbe6e7 is described below
commit 1dbe6e7c71953f74876a7fb09606906c4e149bf1
Author: Volkan Yazıcı <vo...@gmail.com>
AuthorDate: Mon May 10 12:32:43 2021 +0200
LOG4J2-3051 Add CaseConverterResolver to JsonTemplateLayout (#490)
---
.../json/resolver/CaseConverterResolver.java | 309 +++++++++++++++++++++
.../resolver/CaseConverterResolverFactory.java | 34 +++
.../json/resolver/TemplateResolverConfig.java | 25 ++
.../template/json/resolver/TemplateResolvers.java | 17 +-
.../template/json/resolver/TimestampResolver.java | 16 +-
.../layout/template/json/util/JsonReader.java | 12 +-
.../log4j/layout/template/json/EcsLayoutTest.java | 6 +-
.../log4j/layout/template/json/GelfLayoutTest.java | 6 +-
.../log4j/layout/template/json/JsonLayoutTest.java | 6 +-
.../template/json/JsonTemplateLayoutTest.java | 58 +---
.../template/json/LayoutComparisonHelpers.java | 37 ---
.../log4j/layout/template/json/TestHelpers.java | 75 +++++
.../json/resolver/CaseConverterResolverTest.java | 125 +++++++++
.../layout/template/json/util/JsonReaderTest.java | 8 +
src/changes/changes.xml | 3 +
.../asciidoc/manual/json-template-layout.adoc.vm | 131 ++++++++-
src/site/xdoc/manual/configuration.xml.vm | 2 +-
17 files changed, 744 insertions(+), 126 deletions(-)
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CaseConverterResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CaseConverterResolver.java
new file mode 100644
index 0000000..a8e09be
--- /dev/null
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CaseConverterResolver.java
@@ -0,0 +1,309 @@
+package org.apache.logging.log4j.layout.template.json.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.layout.template.json.JsonTemplateLayoutDefaults;
+import org.apache.logging.log4j.layout.template.json.util.JsonReader;
+import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
+
+import java.util.Locale;
+import java.util.function.Function;
+
+/**
+ * Converts the case of string values.
+ *
+ * <h3>Configuration</h3>
+ *
+ * <pre>
+ * config = case , input , [ locale ] , [ errorHandlingStrategy ]
+ * input = JSON
+ * case = "case" -> ( "upper" | "lower" )
+ * locale = "locale" -> (
+ * language |
+ * ( language , "_" , country ) |
+ * ( language , "_" , country , "_" , variant )
+ * )
+ * errorHandlingStrategy = "errorHandlingStrategy" -> (
+ * "fail" |
+ * "pass" |
+ * "replace"
+ * )
+ * replacement = "replacement" -> JSON
+ *
+ * </pre>
+ *
+ * {@code input} can be any available template value; e.g., a JSON literal,
+ * a lookup string, an object pointing to another resolver.
+ * <p>
+ * Unless provided, {@code locale} points to the one returned by
+ * {@link JsonTemplateLayoutDefaults#getLocale()}, which is configured by
+ * {@code log4j.layout.jsonTemplate.locale} system property and by default set
+ * to the default system locale.
+ * <p>
+ * {@code errorHandlingStrategy} determines the behavior when either the
+ * {@code input} doesn't resolve to a string value or case conversion throws an
+ * exception:
+ * <ul>
+ * <li>{@code fail} propagates the failure
+ * <li>{@code pass} causes the resolved value to be passed as is
+ * <li>{@code replace} suppresses the failure and replaces it with the
+ * {@code replacement}, which is set to {@code null} by default
+ * </ul>
+ * {@code errorHandlingStrategy} is set to {@code replace} by default.
+ * <p>
+ * Most of the time JSON logs are persisted to a storage solution
+ * (e.g., Elasticsearch) that keeps a statically-typed index on fields.
+ * Hence, if a field is always expected to be of type string, using non-string
+ * {@code replacement}s or {@code pass} in {@code errorHandlingStrategy} might
+ * result in type incompatibility issues at the storage level.
+ * <p>
+ * Unless the {@code input} value is {@code pass}ed intact or {@code replace}d,
+ * case conversion is not garbage-free.
+ *
+ * <h3>Examples</h3>
+ *
+ * Convert the resolved log level strings to upper-case:
+ *
+ * <pre>
+ * {
+ * "$resolver": "caseConverter",
+ * "case": "upper",
+ * "input": {
+ * "$resolver": "level",
+ * "field": "name"
+ * }
+ * }
+ * </pre>
+ *
+ * Convert the resolved {@code USER} environment variable to lower-case using
+ * {@code nl_NL} locale:
+ *
+ * <pre>
+ * {
+ * "$resolver": "caseConverter",
+ * "case": "lower",
+ * "locale": "nl_NL",
+ * "input": "${env:USER}"
+ * }
+ * </pre>
+ *
+ * Convert the resolved {@code sessionId} thread context data (MDC) to
+ * lower-case:
+ *
+ * <pre>
+ * {
+ * "$resolver": "caseConverter",
+ * "case": "lower",
+ * "input": {
+ * "$resolver": "mdc",
+ * "key": "sessionId"
+ * }
+ * }
+ * </pre>
+ *
+ * Above, if {@code sessionId} MDC resolves to a, say, number, case conversion
+ * will fail. Since {@code errorHandlingStrategy} is set to {@code replace} and
+ * {@code replacement} is set to {@code null} by default, the resolved value
+ * will be {@code null}. One can suppress this behavior and let the resolved
+ * {@code sessionId} number be left as is:
+ *
+ * <pre>
+ * {
+ * "$resolver": "caseConverter",
+ * "case": "lower",
+ * "input": {
+ * "$resolver": "mdc",
+ * "key": "sessionId"
+ * },
+ * "errorHandlingStrategy": "pass"
+ * }
+ * </pre>
+ *
+ * or replace it with a custom string:
+ *
+ * <pre>
+ * {
+ * "$resolver": "caseConverter",
+ * "case": "lower",
+ * "input": {
+ * "$resolver": "mdc",
+ * "key": "sessionId"
+ * },
+ * "errorHandlingStrategy": "replace"
+ * "replacement": "unknown"
+ * }
+ * </pre>
+ */
+public final class CaseConverterResolver implements EventResolver {
+
+ private final TemplateResolver<LogEvent> inputResolver;
+
+ private final Function<String, String> converter;
+
+ private final ErrorHandlingStrategy errorHandlingStrategy;
+
+ private final TemplateResolver<LogEvent> replacementResolver;
+
+ private enum ErrorHandlingStrategy {
+
+ FAIL("fail"),
+
+ PASS("pass"),
+
+ REPLACE("replace");
+
+ private final String name;
+
+ ErrorHandlingStrategy(String name) {
+ this.name = name;
+ }
+
+ }
+
+ CaseConverterResolver(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ this.inputResolver = createDelegate(context, config);
+ this.converter = createConverter(config);
+ this.errorHandlingStrategy = readErrorHandlingStrategy(config);
+ this.replacementResolver = createReplacement(context, config);
+ }
+
+ private static TemplateResolver<LogEvent> createDelegate(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ Object delegateObject = config.getObject("input");
+ return TemplateResolvers.ofObject(context, delegateObject);
+ }
+
+ private static Function<String, String> createConverter(
+ final TemplateResolverConfig config) {
+ final Locale locale = config.getLocale("locale");
+ final String _case = config.getString("case");
+ if ("upper".equals(_case)) {
+ return input -> input.toUpperCase(locale);
+ } else if ("lower".equals(_case)) {
+ return input -> input.toLowerCase(locale);
+ } else {
+ throw new IllegalArgumentException("invalid case: " + config);
+ }
+ }
+
+ private static ErrorHandlingStrategy readErrorHandlingStrategy(
+ final TemplateResolverConfig config) {
+ final String strategyName = config.getString("errorHandlingStrategy");
+ if (strategyName == null) {
+ return ErrorHandlingStrategy.REPLACE;
+ }
+ for (ErrorHandlingStrategy strategy : ErrorHandlingStrategy.values()) {
+ if (strategy.name.equals(strategyName)) {
+ return strategy;
+ }
+ }
+ throw new IllegalArgumentException(
+ "illegal error handling strategy: " + config);
+ }
+
+ private static TemplateResolver<LogEvent> createReplacement(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ Object replacementObject = config.getObject("replacement");
+ return TemplateResolvers.ofObject(context, replacementObject);
+ }
+
+ static String getName() {
+ return "caseConverter";
+ }
+
+ @Override
+ public boolean isFlattening() {
+ return inputResolver.isFlattening();
+ }
+
+ @Override
+ public boolean isResolvable() {
+ return inputResolver.isResolvable();
+ }
+
+ @Override
+ public boolean isResolvable(final LogEvent logEvent) {
+ return inputResolver.isResolvable(logEvent);
+ }
+
+ @Override
+ public void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
+ final int startIndex = jsonWriter.getStringBuilder().length();
+ inputResolver.resolve(logEvent, jsonWriter);
+ convertCase(logEvent, jsonWriter, startIndex);
+ }
+
+ @Override
+ public void resolve(
+ final LogEvent logEvent,
+ final JsonWriter jsonWriter,
+ final boolean succeedingEntry) {
+ final int startIndex = jsonWriter.getStringBuilder().length();
+ inputResolver.resolve(logEvent, jsonWriter, succeedingEntry);
+ convertCase(logEvent, jsonWriter, startIndex);
+ }
+
+ private void convertCase(
+ final LogEvent logEvent,
+ final JsonWriter jsonWriter,
+ final int startIndex) {
+
+ // If the last emitted JSON token was a string, convert it.
+ final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
+ final int endIndex = jsonWriterStringBuilder.length();
+ final boolean stringTyped = (startIndex + 1) < endIndex
+ && jsonWriterStringBuilder.charAt(startIndex) == '"'
+ && jsonWriterStringBuilder.charAt(endIndex - 1) == '"';
+ if (stringTyped) {
+ final String json = jsonWriterStringBuilder.substring(startIndex, endIndex);
+ convertCase(logEvent, jsonWriter, startIndex, json);
+ }
+
+ // Otherwise, see what we can do.
+ else if (ErrorHandlingStrategy.FAIL.equals(errorHandlingStrategy)) {
+ final String json = jsonWriterStringBuilder.substring(startIndex, endIndex);
+ throw new RuntimeException(
+ "was expecting a string value, found: " + json);
+ } else if (ErrorHandlingStrategy.PASS.equals(errorHandlingStrategy)) {
+ // Do nothing.
+ } else if (ErrorHandlingStrategy.REPLACE.equals(errorHandlingStrategy)) {
+ jsonWriterStringBuilder.setLength(startIndex);
+ replacementResolver.resolve(logEvent, jsonWriter);
+ } else {
+ throw new AssertionError("should not have reached here");
+ }
+
+ }
+
+ private void convertCase(
+ final LogEvent logEvent,
+ final JsonWriter jsonWriter,
+ final int startIndex,
+ final String json) {
+ final StringBuilder jsonWriterStringBuilder = jsonWriter.getStringBuilder();
+ final String string = (String) JsonReader.read(json);
+ final String convertedString;
+ try {
+ convertedString = converter.apply(string);
+ } catch (final Exception error) {
+ if (ErrorHandlingStrategy.FAIL.equals(errorHandlingStrategy)) {
+ throw new RuntimeException(
+ "case conversion failure for string: " + string,
+ error);
+ } else if (ErrorHandlingStrategy.PASS.equals(errorHandlingStrategy)) {
+ return;
+ } else if (ErrorHandlingStrategy.REPLACE.equals(errorHandlingStrategy)) {
+ jsonWriterStringBuilder.setLength(startIndex);
+ replacementResolver.resolve(logEvent, jsonWriter);
+ return;
+ }
+ throw new AssertionError("should not have reached here");
+ }
+ jsonWriterStringBuilder.setLength(startIndex);
+ jsonWriter.writeString(convertedString);
+ }
+
+}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CaseConverterResolverFactory.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CaseConverterResolverFactory.java
new file mode 100644
index 0000000..3d0395a
--- /dev/null
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CaseConverterResolverFactory.java
@@ -0,0 +1,34 @@
+package org.apache.logging.log4j.layout.template.json.resolver;
+
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * {@link CaseConverterResolver} factory.
+ */
+@Plugin(name = "CaseConverterResolverFactory", category = TemplateResolverFactory.CATEGORY)
+public final class CaseConverterResolverFactory implements EventResolverFactory {
+
+ private static final CaseConverterResolverFactory INSTANCE =
+ new CaseConverterResolverFactory();
+
+ private CaseConverterResolverFactory() {}
+
+ @PluginFactory
+ public static CaseConverterResolverFactory getInstance() {
+ return INSTANCE;
+ }
+
+ @Override
+ public String getName() {
+ return CaseConverterResolver.getName();
+ }
+
+ @Override
+ public CaseConverterResolver create(
+ final EventResolverContext context,
+ final TemplateResolverConfig config) {
+ return new CaseConverterResolver(context, config);
+ }
+
+}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverConfig.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverConfig.java
index 1ca0c9b..fc9d6df 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverConfig.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolverConfig.java
@@ -16,8 +16,11 @@
*/
package org.apache.logging.log4j.layout.template.json.resolver;
+import org.apache.logging.log4j.layout.template.json.JsonTemplateLayoutDefaults;
import org.apache.logging.log4j.layout.template.json.util.MapAccessor;
+import java.util.Arrays;
+import java.util.Locale;
import java.util.Map;
/**
@@ -61,4 +64,26 @@ public class TemplateResolverConfig extends MapAccessor {
super(map);
}
+ public Locale getLocale(final String key) {
+ final String[] path = {key};
+ return getLocale(path);
+ }
+
+ public Locale getLocale(final String[] path) {
+ final String spec = getString(path);
+ if (spec == null) {
+ return JsonTemplateLayoutDefaults.getLocale();
+ }
+ final String[] specFields = spec.split("_", 3);
+ switch (specFields.length) {
+ case 1: return new Locale(specFields[0]);
+ case 2: return new Locale(specFields[0], specFields[1]);
+ case 3: return new Locale(specFields[0], specFields[1], specFields[2]);
+ }
+ final String message = String.format(
+ "was expecting a locale at path %s: %s",
+ Arrays.asList(path), this);
+ throw new IllegalArgumentException(message);
+ }
+
}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolvers.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolvers.java
index 27a5b49..20ad802 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolvers.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TemplateResolvers.java
@@ -22,6 +22,7 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.stream.Collectors;
/**
@@ -78,6 +79,10 @@ public final class TemplateResolvers {
final C context,
final String template) {
+ // Check arguments.
+ Objects.requireNonNull(context, "context");
+ Objects.requireNonNull(template, "template");
+
// Read the template.
Object node;
try {
@@ -104,7 +109,7 @@ public final class TemplateResolvers {
}
- private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofObject(
+ static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofObject(
final C context,
final Object object) {
if (object == null) {
@@ -289,10 +294,14 @@ public final class TemplateResolvers {
private static <V, C extends TemplateResolverContext<V, C>> TemplateResolver<V> ofResolver(
final C context,
- final Map<String, Object> map) {
+ final Map<String, Object> configMap) {
+
+ // Check arguments.
+ Objects.requireNonNull(context, "context");
+ Objects.requireNonNull(configMap, "configMap");
// Extract the resolver name.
- final Object resolverNameObject = map.get(RESOLVER_FIELD_NAME);
+ final Object resolverNameObject = configMap.get(RESOLVER_FIELD_NAME);
if (!(resolverNameObject instanceof String)) {
throw new IllegalArgumentException(
"invalid resolver name: " + resolverNameObject);
@@ -305,7 +314,7 @@ public final class TemplateResolvers {
if (resolverFactory == null) {
throw new IllegalArgumentException("unknown resolver: " + resolverName);
}
- final TemplateResolverConfig resolverConfig = new TemplateResolverConfig(map);
+ final TemplateResolverConfig resolverConfig = new TemplateResolverConfig(configMap);
return resolverFactory.create(context, resolverConfig);
}
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
index fece006..fe0e2ca 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/TimestampResolver.java
@@ -236,7 +236,7 @@ public final class TimestampResolver implements EventResolver {
final TemplateResolverConfig config) {
final String format = readFormat(config);
final TimeZone timeZone = readTimeZone(config);
- final Locale locale = readLocale(config);
+ final Locale locale = config.getLocale(new String[]{"pattern", "locale"});
final FastDateFormat fastDateFormat =
FastDateFormat.getInstance(format, timeZone, locale);
return new FormatResolverContext(timeZone, locale, fastDateFormat);
@@ -276,20 +276,6 @@ public final class TimestampResolver implements EventResolver {
return TimeZone.getTimeZone(timeZoneId);
}
- private static Locale readLocale(final TemplateResolverConfig config) {
- final String locale = config.getString(new String[]{"pattern", "locale"});
- if (locale == null) {
- return JsonTemplateLayoutDefaults.getLocale();
- }
- final String[] localeFields = locale.split("_", 3);
- switch (localeFields.length) {
- case 1: return new Locale(localeFields[0]);
- case 2: return new Locale(localeFields[0], localeFields[1]);
- case 3: return new Locale(localeFields[0], localeFields[1], localeFields[2]);
- }
- throw new IllegalArgumentException("invalid timestamp locale: " + config);
- }
-
}
/**
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/JsonReader.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/JsonReader.java
index cfc3fdc..c1377f7 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/JsonReader.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/JsonReader.java
@@ -24,6 +24,7 @@ import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
/**
* A simple JSON parser mapping tokens to basic Java types.
@@ -88,13 +89,16 @@ public final class JsonReader {
private Object readToken;
- private StringBuilder buffer = new StringBuilder();
+ private final StringBuilder buffer;
- private JsonReader() {}
+ private JsonReader() {
+ this.buffer = new StringBuilder();
+ }
- public static Object read(final String string) {
+ public static Object read(final String json) {
+ Objects.requireNonNull(json, "json");
final JsonReader reader = new JsonReader();
- return reader.read(new StringCharacterIterator(string));
+ return reader.read(new StringCharacterIterator(json));
}
private Object read(final CharacterIterator ci) {
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/EcsLayoutTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/EcsLayoutTest.java
index f58e3c4..6e59620 100644
--- a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/EcsLayoutTest.java
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/EcsLayoutTest.java
@@ -30,7 +30,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
-import static org.apache.logging.log4j.layout.template.json.LayoutComparisonHelpers.renderUsing;
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.serializeUsingLayout;
class EcsLayoutTest {
@@ -100,12 +100,12 @@ class EcsLayoutTest {
private static Map<String, Object> renderUsingJsonTemplateLayout(
final LogEvent logEvent) {
- return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
+ return serializeUsingLayout(logEvent, JSON_TEMPLATE_LAYOUT);
}
private static Map<String, Object> renderUsingEcsLayout(
final LogEvent logEvent) {
- return renderUsing(logEvent, ECS_LAYOUT);
+ return serializeUsingLayout(logEvent, ECS_LAYOUT);
}
}
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/GelfLayoutTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/GelfLayoutTest.java
index 7601d44..45355d7 100644
--- a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/GelfLayoutTest.java
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/GelfLayoutTest.java
@@ -30,7 +30,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
-import static org.apache.logging.log4j.layout.template.json.LayoutComparisonHelpers.renderUsing;
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.serializeUsingLayout;
class GelfLayoutTest {
@@ -86,12 +86,12 @@ class GelfLayoutTest {
private static Map<String, Object> renderUsingJsonTemplateLayout(
final LogEvent logEvent) {
- return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
+ return serializeUsingLayout(logEvent, JSON_TEMPLATE_LAYOUT);
}
private static Map<String, Object> renderUsingGelfLayout(
final LogEvent logEvent) {
- return renderUsing(logEvent, GELF_LAYOUT);
+ return serializeUsingLayout(logEvent, GELF_LAYOUT);
}
/**
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonLayoutTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonLayoutTest.java
index 90288f1..938b275 100644
--- a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonLayoutTest.java
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonLayoutTest.java
@@ -27,7 +27,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
-import static org.apache.logging.log4j.layout.template.json.LayoutComparisonHelpers.renderUsing;
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.serializeUsingLayout;
class JsonLayoutTest {
@@ -76,12 +76,12 @@ class JsonLayoutTest {
private static Map<String, Object> renderUsingJsonTemplateLayout(
final LogEvent logEvent) {
- return renderUsing(logEvent, JSON_TEMPLATE_LAYOUT);
+ return serializeUsingLayout(logEvent, JSON_TEMPLATE_LAYOUT);
}
private static Map<String, Object> renderUsingJsonLayout(
final LogEvent logEvent) {
- return renderUsing(logEvent, JSON_LAYOUT);
+ return serializeUsingLayout(logEvent, JSON_LAYOUT);
}
}
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java
index d75e0ab..190f695 100644
--- a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayoutTest.java
@@ -22,11 +22,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;
-import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.SocketAppender;
import org.apache.logging.log4j.core.config.Configuration;
-import org.apache.logging.log4j.core.config.DefaultConfiguration;
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
@@ -42,9 +40,7 @@ import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFacto
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver;
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig;
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory;
-import org.apache.logging.log4j.layout.template.json.util.JsonReader;
import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
-import org.apache.logging.log4j.layout.template.json.util.MapAccessor;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.MessageFactory;
import org.apache.logging.log4j.message.ObjectMessage;
@@ -82,25 +78,20 @@ import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.CONFIGURATION;
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.asMap;
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.usingSerializedLogEventAccessor;
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.writeJson;
import static org.assertj.core.api.Assertions.assertThat;
@SuppressWarnings("DoubleBraceInitialization")
class JsonTemplateLayoutTest {
- private static final Configuration CONFIGURATION = new DefaultConfiguration();
-
private static final List<LogEvent> LOG_EVENTS = LogEventFixture.createFullLogEvents(5);
- private static final JsonWriter JSON_WRITER = JsonWriter
- .newBuilder()
- .setMaxStringLength(10_000)
- .setTruncatedStringSuffix("…")
- .build();
-
private static final ObjectMapper OBJECT_MAPPER = JacksonFixture.getObjectMapper();
private static final String LOGGER_NAME = JsonTemplateLayoutTest.class.getSimpleName();
@@ -1863,7 +1854,6 @@ class JsonTemplateLayoutTest {
.build();
// Check the serialized log event.
- final String expectedClassName = JsonTemplateLayoutTest.class.getCanonicalName();
usingSerializedLogEventAccessor(layout, logEvent, accessor -> Assertions
.assertThat(accessor.getString("customField"))
.matches("CustomValue-[0-9]+"));
@@ -2392,44 +2382,4 @@ class JsonTemplateLayoutTest {
}
- private static synchronized String writeJson(final Object value) {
- final StringBuilder stringBuilder = JSON_WRITER.getStringBuilder();
- stringBuilder.setLength(0);
- try {
- JSON_WRITER.writeValue(value);
- return stringBuilder.toString();
- } finally {
- stringBuilder.setLength(0);
- }
- }
-
- private static void usingSerializedLogEventAccessor(
- final Layout<String> layout,
- final LogEvent logEvent,
- final Consumer<MapAccessor> accessorConsumer) {
- final String serializedLogEventJson = layout.toSerializable(logEvent);
- @SuppressWarnings("unchecked")
- final Map<String, Object> deserializedLogEvent =
- (Map<String, Object>) readJson(serializedLogEventJson);
- final MapAccessor serializedLogEventAccessor = new MapAccessor(deserializedLogEvent);
- accessorConsumer.accept(serializedLogEventAccessor);
- }
-
- private static Object readJson(final String json) {
- return JsonReader.read(json);
- }
-
- private static Map<String, Object> asMap(final Object... pairs) {
- final Map<String, Object> map = new LinkedHashMap<>();
- if (pairs.length % 2 != 0) {
- throw new IllegalArgumentException("odd number of arguments: " + pairs.length);
- }
- for (int i = 0; i < pairs.length; i += 2) {
- final String key = (String) pairs[i];
- final Object value = pairs[i + 1];
- map.put(key, value);
- }
- return map;
- }
-
}
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/LayoutComparisonHelpers.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/LayoutComparisonHelpers.java
deleted file mode 100644
index 1c343c5..0000000
--- a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/LayoutComparisonHelpers.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-package org.apache.logging.log4j.layout.template.json;
-
-import org.apache.logging.log4j.core.Layout;
-import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.layout.template.json.util.JsonReader;
-
-import java.util.Map;
-
-final class LayoutComparisonHelpers {
-
- private LayoutComparisonHelpers() {}
-
- @SuppressWarnings("unchecked")
- static Map<String, Object> renderUsing(
- final LogEvent logEvent,
- final Layout<String> layout) {
- final String json = layout.toSerializable(logEvent);
- return (Map<String, Object>) JsonReader.read(json);
- }
-
-}
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/TestHelpers.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/TestHelpers.java
new file mode 100644
index 0000000..78404b6
--- /dev/null
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/TestHelpers.java
@@ -0,0 +1,75 @@
+package org.apache.logging.log4j.layout.template.json;
+
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.layout.template.json.util.JsonReader;
+import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
+import org.apache.logging.log4j.layout.template.json.util.MapAccessor;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+public final class TestHelpers {
+
+ public static final Configuration CONFIGURATION = new DefaultConfiguration();
+
+ private static final JsonWriter JSON_WRITER = JsonWriter
+ .newBuilder()
+ .setMaxStringLength(10_000)
+ .setTruncatedStringSuffix("…")
+ .build();
+
+ private TestHelpers() {}
+
+ @SuppressWarnings("unchecked")
+ static Map<String, Object> serializeUsingLayout(
+ final LogEvent logEvent,
+ final Layout<String> layout) {
+ final String json = layout.toSerializable(logEvent);
+ return (Map<String, Object>) JsonReader.read(json);
+ }
+
+ public static synchronized String writeJson(final Object value) {
+ final StringBuilder stringBuilder = JSON_WRITER.getStringBuilder();
+ stringBuilder.setLength(0);
+ try {
+ JSON_WRITER.writeValue(value);
+ return stringBuilder.toString();
+ } finally {
+ stringBuilder.setLength(0);
+ }
+ }
+
+ public static void usingSerializedLogEventAccessor(
+ final Layout<String> layout,
+ final LogEvent logEvent,
+ final Consumer<MapAccessor> accessorConsumer) {
+ final String serializedLogEventJson = layout.toSerializable(logEvent);
+ @SuppressWarnings("unchecked")
+ final Map<String, Object> deserializedLogEvent =
+ (Map<String, Object>) readJson(serializedLogEventJson);
+ final MapAccessor serializedLogEventAccessor = new MapAccessor(deserializedLogEvent);
+ accessorConsumer.accept(serializedLogEventAccessor);
+ }
+
+ public static Object readJson(final String json) {
+ return JsonReader.read(json);
+ }
+
+ public static Map<String, Object> asMap(final Object... pairs) {
+ final Map<String, Object> map = new LinkedHashMap<>();
+ if (pairs.length % 2 != 0) {
+ throw new IllegalArgumentException("odd number of arguments: " + pairs.length);
+ }
+ for (int i = 0; i < pairs.length; i += 2) {
+ final String key = (String) pairs[i];
+ final Object value = pairs[i + 1];
+ map.put(key, value);
+ }
+ return map;
+ }
+
+}
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/CaseConverterResolverTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/CaseConverterResolverTest.java
new file mode 100644
index 0000000..a52659c
--- /dev/null
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/resolver/CaseConverterResolverTest.java
@@ -0,0 +1,125 @@
+package org.apache.logging.log4j.layout.template.json.resolver;
+
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.layout.template.json.JsonTemplateLayout;
+import org.apache.logging.log4j.util.SortedArrayStringMap;
+import org.apache.logging.log4j.util.StringMap;
+import org.apache.logging.log4j.util.Strings;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.CONFIGURATION;
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.asMap;
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.readJson;
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.usingSerializedLogEventAccessor;
+import static org.apache.logging.log4j.layout.template.json.TestHelpers.writeJson;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class CaseConverterResolverTest {
+
+ @ParameterizedTest
+ @CsvSource({
+ // case | locale | input | output
+ "upper" + ",nl" + ",ioz" + ",IOZ",
+ "upper" + ",nl" + ",IOZ" + ",IOZ",
+ "lower" + ",nl" + ",ioz" + ",ioz",
+ "lower" + ",nl" + ",IOZ" + ",ioz",
+ "upper" + ",tr" + ",ıiğüşöç" + ",IİĞÜŞÖÇ",
+ "upper" + ",tr" + ",IİĞÜŞÖÇ" + ",IİĞÜŞÖÇ",
+ "lower" + ",tr" + ",ıiğüşöç" + ",ıiğüşöç",
+ "lower" + ",tr" + ",IİĞÜŞÖÇ" + ",ıiğüşöç"
+ })
+ void test_upper(
+ final String case_,
+ final String locale,
+ final String input,
+ final String output) {
+
+ // Create the event template.
+ final String eventTemplate = writeJson(asMap(
+ "output", asMap(
+ "$resolver", "caseConverter",
+ "case", case_,
+ "locale", locale,
+ "input", asMap(
+ "$resolver", "mdc",
+ "key", "input"))));
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Create the log event.
+ final StringMap contextData = new SortedArrayStringMap();
+ contextData.putValue("input", input);
+ final LogEvent logEvent = Log4jLogEvent
+ .newBuilder()
+ .setContextData(contextData)
+ .build();
+
+ // Check the serialized event.
+ usingSerializedLogEventAccessor(layout, logEvent, accessor ->
+ assertThat(accessor.getString("output")).isEqualTo(output));
+
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ // failure message | locale | input | strategy | replacement | output
+ "" + ",nl" + ",1" + ",pass" + ",null" + ",1",
+ "" + ",nl" + ",[2]" + ",pass" + ",null" + ",[2]",
+ "was expecting a string value" + ",nl" + ",1" + ",fail" + ",null" + ",null",
+ "" + ",nl" + ",1" + ",replace" + ",null" + ",null",
+ "" + ",nl" + ",1" + ",replace" + ",2" + ",2",
+ "" + ",nl" + ",1" + ",replace" + ",\"s\"" + ",\"s\""
+ })
+ void test_errorHandlingStrategy(
+ final String failureMessage,
+ final String locale,
+ final String inputJson,
+ final String errorHandlingStrategy,
+ final String replacementJson,
+ final String outputJson) {
+
+ // Parse arguments.
+ final Object input = readJson(inputJson);
+ final Object replacement = readJson(replacementJson);
+ final Object output = readJson(outputJson);
+
+ // Create the event template.
+ final String eventTemplate = writeJson(asMap(
+ "output", asMap(
+ "$resolver", "caseConverter",
+ "case", "lower",
+ "locale", locale,
+ "input", input,
+ "errorHandlingStrategy", errorHandlingStrategy,
+ "replacement", replacement)));
+
+ // Create the layout.
+ final JsonTemplateLayout layout = JsonTemplateLayout
+ .newBuilder()
+ .setConfiguration(CONFIGURATION)
+ .setEventTemplate(eventTemplate)
+ .build();
+
+ // Create the log event.
+ final LogEvent logEvent = Log4jLogEvent.newBuilder().build();
+
+ // Check the serialized event.
+ final boolean failureExpected = Strings.isNotBlank(failureMessage);
+ if (failureExpected) {
+ assertThatThrownBy(() -> layout.toSerializable(logEvent))
+ .hasMessageContaining(failureMessage);
+ } else {
+ usingSerializedLogEventAccessor(layout, logEvent, accessor ->
+ assertThat(accessor.getObject("output")).isEqualTo(output));
+ }
+ }
+
+}
diff --git a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/util/JsonReaderTest.java b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/util/JsonReaderTest.java
index aa7992e..2c35e5a 100644
--- a/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/util/JsonReaderTest.java
+++ b/log4j-layout-template-json/src/test/java/org/apache/logging/log4j/layout/template/json/util/JsonReaderTest.java
@@ -28,6 +28,14 @@ import java.util.LinkedHashMap;
class JsonReaderTest {
@Test
+ void test_null() {
+ Assertions
+ .assertThatThrownBy(() -> JsonReader.read(null))
+ .isInstanceOf(NullPointerException.class)
+ .hasMessage("json");
+ }
+
+ @Test
void test_valid_null() {
test("null", null);
test("[null, null]", Arrays.asList(null, null));
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 32f569f..cfdaa03 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -31,6 +31,9 @@
-->
<release version="2.15.0" date="2021-MM-DD" description="GA Release 2.15.0">
<!-- ADDS -->
+ <action issue="LOG4J2-3051" dev="vy" type="add">
+ Add CaseConverterResolver to JsonTemplateLayout.
+ </action>
<action issue="LOG4J2-3064" dev="rgoers" type="add">
Add Arbiters and SpringProfile plugin.
</action>
diff --git a/src/site/asciidoc/manual/json-template-layout.adoc.vm b/src/site/asciidoc/manual/json-template-layout.adoc.vm
index c3a9e05..97e9a05 100644
--- a/src/site/asciidoc/manual/json-template-layout.adoc.vm
+++ b/src/site/asciidoc/manual/json-template-layout.adoc.vm
@@ -454,11 +454,136 @@ similar to the following:
The complete list of available event template resolvers are provided below in
detail.
+[#event-template-resolver-caseConverter]
+===== `caseConverter`
+
+[source]
+----
+config = case , input , [ locale ] , [ errorHandlingStrategy ]
+input = JSON
+case = "case" -> ( "upper" | "lower" )
+locale = "locale" -> (
+ language |
+ ( language , "_" , country ) |
+ ( language , "_" , country , "_" , variant )
+ )
+errorHandlingStrategy = "errorHandlingStrategy" -> (
+ "fail" |
+ "pass" |
+ "replace"
+ )
+replacement = "replacement" -> JSON
+----
+
+Converts the case of string values.
+
+`input` can be any available template value; e.g., a JSON literal, a lookup
+string, an object pointing to another resolver.
+
+Unless provided, `locale` points to the one returned by
+`JsonTemplateLayoutDefaults.getLocale()`, which is configured by
+`log4j.layout.jsonTemplate.locale` system property and by default set to the
+default system locale.
+
+`errorHandlingStrategy` determines the behavior when either the input doesn't
+resolve to a string value or case conversion throws an exception:
+
+* `fail` propagates the failure
+* `pass` causes the resolved value to be passed as is
+* `replace` suppresses the failure and replaces it with the `replacement`,
+which is set to `null` by default
+
+`errorHandlingStrategy` is set to `replace` by default.
+
+Most of the time JSON logs are persisted to a storage solution (e.g.,
+Elasticsearch) that keeps a statically-typed index on fields. Hence, if a field
+is always expected to be of type string, using non-string ``replacement``s or
+`pass` in `errorHandlingStrategy` might result in type incompatibility issues at
+the storage level.
+
+Unless the input value is ``pass``ed intact or ``replace``d, case conversion is
+not garbage-free.
+
+====== Examples
+
+Convert the resolved log level strings to upper-case:
+
+[source,json]
+----
+{
+ "$resolver": "caseConverter",
+ "case": "upper",
+ "input": {
+ "$resolver": "level",
+ "field": "name"
+ }
+}
+----
+
+Convert the resolved `USER` environment variable to lower-case using `nl_NL`
+locale:
+
+[source,json]
+----
+{
+ "$resolver": "caseConverter",
+ "case": "lower",
+ "locale": "nl_NL",
+ "input": "${dollar}{env:USER}"
+}
+----
+
+Convert the resolved `sessionId` thread context data (MDC) to lower-case:
+
+[source,json]
+----
+{
+ "$resolver": "caseConverter",
+ "case": "lower",
+ "input": {
+ "$resolver": "mdc",
+ "key": "sessionId"
+ }
+}
+----
+
+Above, if `sessionId` MDC resolves to a, say, number, case conversion will fail.
+Since `errorHandlingStrategy` is set to `replace` and replacement is set to
+`null` by default, the resolved value will be `null`. One can suppress this
+behavior and let the resolved `sessionId` number be left as is:
+
+[source,json]
+----
+{
+ "$resolver": "caseConverter",
+ "case": "lower",
+ "input": {
+ "$resolver": "mdc",
+ "key": "sessionId"
+ },
+ "errorHandlingStrategy": "pass"
+}
+----
+
+or replace it with a custom string:
+
+[source,json]
+----
+{
+ "$resolver": "caseConverter",
+ "case": "lower",
+ "input": {
+ "$resolver": "mdc",
+ "key": "sessionId"
+ },
+ "errorHandlingStrategy": "replace",
+ "replacement": "unknown"
+}
+----
+
[#event-template-resolver-endOfBatch]
===== `endOfBatch`
-Resolves `logEvent.isEndOfBatch()` boolean flag:
-
[source,json]
----
{
@@ -466,6 +591,8 @@ Resolves `logEvent.isEndOfBatch()` boolean flag:
}
----
+Resolves `logEvent.isEndOfBatch()` boolean flag.
+
[#event-template-resolver-exception]
===== `exception`
diff --git a/src/site/xdoc/manual/configuration.xml.vm b/src/site/xdoc/manual/configuration.xml.vm
index 0b6e77e..5530ac8 100644
--- a/src/site/xdoc/manual/configuration.xml.vm
+++ b/src/site/xdoc/manual/configuration.xml.vm
@@ -1904,7 +1904,7 @@ public class AwesomeTest {
Available context selector implementation classes:<br />
<!-- deliberately inserted spaces to allow line break -->
<tt>org.apache.logging.log4j.core.async .AsyncLoggerContextSelector</tt> - makes <a href="async.html">all loggers asynchronous</a>.<br />
- <tt>org.apache.logging.log4j.core.async .BasicAsyncLoggerContextSelector</tt> - makes <a href="async.html"> all loggers asynchronous using a single shared AsyncLoggerContext.</a><br />
+ <tt>org.apache.logging.log4j.core.async .BasicAsyncLoggerContextSelector</tt> - makes <a href="async.html">all loggers asynchronous</a> using a single shared AsyncLoggerContext.<br />
<tt>org.apache.logging.log4j.core.selector .BasicContextSelector</tt> - creates a single shared LoggerContext.<br />
<tt>org.apache.logging.log4j.core.selector .ClassLoaderContextSelector</tt> - separate LoggerContexts for each web application.<br />
<tt>org.apache.logging.log4j.core.selector .JndiContextSelector</tt> - use JNDI to locate each web application's LoggerContext.<br/>