blob: 521038472991360f8d9df93bb95e33614f30dcbd [file] [log] [blame]
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(final 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) {
final 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 (final 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) {
final 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);
}
}