blob: 05164c9896f66b8cafb5c8c0bfcc52913a6476bc [file] [log] [blame]
/*
* 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.resolver;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
import org.apache.logging.log4j.layout.template.json.util.Recycler;
import org.apache.logging.log4j.layout.template.json.util.RecyclerFactory;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.ReadOnlyStringMap;
import org.apache.logging.log4j.util.TriConsumer;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Pattern;
/**
* {@link ReadOnlyStringMap} resolver.
*
* <h3>Configuration</h3>
*
* <pre>
* config = singleAccess | multiAccess
*
* singleAccess = key , [ stringified ]
* key = "key" -> string
* stringified = "stringified" -> boolean
*
* multiAccess = [ pattern ] , [ flatten ] , [ stringified ]
* pattern = "pattern" -> string
* flatten = "flatten" -> ( boolean | flattenConfig )
* flattenConfig = [ flattenPrefix ]
* flattenPrefix = "prefix" -> string
* </pre>
*
* Note that <tt>singleAccess</tt> resolves a single field, whilst
* <tt>multiAccess</tt> resolves a multitude of fields. If <tt>flatten</tt>
* is provided, <tt>multiAccess</tt> merges the fields with the parent,
* otherwise creates a new JSON object containing the values.
* <p>
* Enabling <tt>stringified</tt> flag converts each value to its string
* representation.
* <p>
* Regex provided in the `pattern` is used to match against the keys.
*
* <h3>Garbage Footprint</h3>
*
* <tt>stringified</tt> allocates a new <tt>String</tt> for values that are not
* of type <tt>String</tt>.
* <p>
* Writing certain non-primitive values (e.g., <tt>BigDecimal</tt>,
* <tt>Set</tt>, etc.) to JSON generates garbage, though most (e.g.,
* <tt>int</tt>, <tt>long</tt>, <tt>String</tt>, <tt>List</tt>,
* <tt>boolean[]</tt>, etc.) don't.
*
* <h3>Examples</h3>
*
* <tt>"$resolver"</tt> is left out in the following examples, since it is to be
* defined by the actual resolver, e.g., {@link MapResolver},
* {@link ThreadContextDataResolver}.
* <p>
* Resolve the value of the field keyed with <tt>userRole</tt>:
*
* <pre>
* {
* "$resolver": "…",
* "key": "userRole"
* }
* </pre>
*
* Resolve the string representation of the <tt>userRank</tt> field value:
*
* <pre>
* {
* "$resolver": "…",
* "key": "userRank",
* "stringified": true
* }
* </pre>
*
* Resolve all fields into an object:
*
* <pre>
* {
* "$resolver": "…"
* }
* </pre>
*
* Resolve all fields into an object such that values are converted to
* string:
*
* <pre>
* {
* "$resolver": "…",
* "stringified": true
* }
* </pre>
*
* Merge all fields whose keys are matching with the
* <tt>user(Role|Rank)</tt> regex into the parent:
*
* <pre>
* {
* "$resolver": "…",
* "flatten": true,
* "pattern": "user(Role|Rank)"
* }
* </pre>
*
* After converting the corresponding field values to string, merge all fields
* to parent such that keys are prefixed with <tt>_</tt>:
*
* <pre>
* {
* "$resolver": "…",
* "stringified": true,
* "flatten": {
* "prefix": "_"
* }
* }
* </pre>
*
* @see MapResolver
* @see ThreadContextDataResolver
*/
class ReadOnlyStringMapResolver implements EventResolver {
private final EventResolver internalResolver;
ReadOnlyStringMapResolver(
final EventResolverContext context,
final TemplateResolverConfig config,
final Function<LogEvent, ReadOnlyStringMap> mapAccessor) {
this.internalResolver = createResolver(context, config, mapAccessor);
}
private static EventResolver createResolver(
final EventResolverContext context,
final TemplateResolverConfig config,
final Function<LogEvent, ReadOnlyStringMap> mapAccessor) {
final Object flattenObject = config.getObject("flatten");
final boolean flatten;
if (flattenObject == null) {
flatten = false;
} else if (flattenObject instanceof Boolean) {
flatten = (boolean) flattenObject;
} else if (flattenObject instanceof Map) {
flatten = true;
} else {
throw new IllegalArgumentException("invalid flatten option: " + config);
}
final String key = config.getString("key");
final String prefix = config.getString(new String[] {"flatten", "prefix"});
final String pattern = config.getString("pattern");
final boolean stringified = config.getBoolean("stringified", false);
if (key != null) {
if (flatten) {
throw new IllegalArgumentException(
"both key and flatten options cannot be supplied: " + config);
}
return createKeyResolver(key, stringified, mapAccessor);
} else {
final RecyclerFactory recyclerFactory = context.getRecyclerFactory();
return createResolver(
recyclerFactory,
flatten,
prefix,
pattern,
stringified,
mapAccessor);
}
}
private static EventResolver createKeyResolver(
final String key,
final boolean stringified,
final Function<LogEvent, ReadOnlyStringMap> mapAccessor) {
return new EventResolver() {
@Override
public boolean isResolvable(final LogEvent logEvent) {
final ReadOnlyStringMap map = mapAccessor.apply(logEvent);
return map != null && map.containsKey(key);
}
@Override
public void resolve(final LogEvent logEvent, final JsonWriter jsonWriter) {
final ReadOnlyStringMap map = mapAccessor.apply(logEvent);
final Object value = map == null ? null : map.getValue(key);
if (stringified) {
final String valueString =
ParameterizedMessage.deepToString(value);
jsonWriter.writeString(valueString);
} else {
jsonWriter.writeValue(value);
}
}
};
}
private static EventResolver createResolver(
final RecyclerFactory recyclerFactory,
final boolean flatten,
final String prefix,
final String pattern,
final boolean stringified,
final Function<LogEvent, ReadOnlyStringMap> mapAccessor) {
// Compile the pattern.
final Pattern compiledPattern =
pattern == null
? null
: Pattern.compile(pattern);
// Create the recycler for the loop context.
final Recycler<LoopContext> loopContextRecycler =
recyclerFactory.create(() -> {
final LoopContext loopContext = new LoopContext();
if (prefix != null) {
loopContext.prefix = prefix;
loopContext.prefixedKey = new StringBuilder(prefix);
}
loopContext.pattern = compiledPattern;
loopContext.stringified = stringified;
return loopContext;
});
// Create the resolver.
return createResolver(flatten, loopContextRecycler, mapAccessor);
}
private static EventResolver createResolver(
final boolean flatten,
final Recycler<LoopContext> loopContextRecycler,
final Function<LogEvent, ReadOnlyStringMap> mapAccessor) {
return new EventResolver() {
@Override
public boolean isFlattening() {
return flatten;
}
@Override
public boolean isResolvable(final LogEvent logEvent) {
final ReadOnlyStringMap map = mapAccessor.apply(logEvent);
return map != null && !map.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 map.
final ReadOnlyStringMap map = mapAccessor.apply(logEvent);
if (map == null || map.isEmpty()) {
if (!flatten) {
jsonWriter.writeNull();
}
return;
}
// Resolve the map.
if (!flatten) {
jsonWriter.writeObjectStart();
}
final LoopContext loopContext = loopContextRecycler.acquire();
loopContext.jsonWriter = jsonWriter;
loopContext.initJsonWriterStringBuilderLength = jsonWriter.getStringBuilder().length();
loopContext.succeedingEntry = flatten && succeedingEntry;
try {
map.forEach(LoopMethod.INSTANCE, loopContext);
} finally {
loopContextRecycler.release(loopContext);
}
if (!flatten) {
jsonWriter.writeObjectEnd();
}
}
};
}
private static final class LoopContext {
private String prefix;
private StringBuilder prefixedKey;
private Pattern pattern;
private boolean stringified;
private JsonWriter jsonWriter;
private int initJsonWriterStringBuilderLength;
private boolean succeedingEntry;
}
private enum LoopMethod implements TriConsumer<String, Object, LoopContext> {
INSTANCE;
@Override
public void accept(
final String key,
final Object value,
final LoopContext loopContext) {
final boolean keyMatched =
loopContext.pattern == null ||
loopContext.pattern.matcher(key).matches();
if (keyMatched) {
final boolean succeedingEntry =
loopContext.succeedingEntry ||
loopContext.initJsonWriterStringBuilderLength <
loopContext.jsonWriter.getStringBuilder().length();
if (succeedingEntry) {
loopContext.jsonWriter.writeSeparator();
}
if (loopContext.prefix == null) {
loopContext.jsonWriter.writeObjectKey(key);
} else {
loopContext.prefixedKey.setLength(loopContext.prefix.length());
loopContext.prefixedKey.append(key);
loopContext.jsonWriter.writeObjectKey(loopContext.prefixedKey);
}
if (loopContext.stringified && !(value instanceof String)) {
final String valueString = ParameterizedMessage.deepToString(value);
loopContext.jsonWriter.writeString(valueString);
} else {
loopContext.jsonWriter.writeValue(value);
}
}
}
}
@Override
public boolean isFlattening() {
return internalResolver.isFlattening();
}
@Override
public boolean isResolvable(final LogEvent logEvent) {
return internalResolver.isResolvable(logEvent);
}
@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);
}
}