All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.logging.log4j.layout.template.json.resolver.ReadOnlyStringMapResolver Maven / Gradle / Ivy

There is a newer version: 3.0.0-beta2
Show newest version
/*
 * 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.util.ReadOnlyStringMap;
import org.apache.logging.log4j.util.TriConsumer;

import java.util.Map;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * {@link ReadOnlyStringMap} resolver.
 *
 * 

Configuration

* *
 * config        = singleAccess | multiAccess
 *
 * singleAccess  = key , [ stringified ]
 * key           = "key" -> string
 * stringified   = "stringified" -> boolean
 *
 * multiAccess   = [ pattern ] , [ replacement ] , [ flatten ] , [ stringified ]
 * pattern       = "pattern" -> string
 * replacement   = "replacement" -> string
 * flatten       = "flatten" -> ( boolean | flattenConfig )
 * flattenConfig = [ flattenPrefix ]
 * flattenPrefix = "prefix" -> string
 * 
* * Note that singleAccess resolves a single field, whilst * multiAccess resolves a multitude of fields. If flatten * is provided, multiAccess merges the fields with the parent, * otherwise creates a new JSON object containing the values. *

* Enabling stringified flag converts each value to its string * representation. *

* Regex provided in the pattern is used to match against the keys. * If provided, replacement will be used to replace the matched keys. * These two are effectively equivalent to * Pattern.compile(pattern).matcher(key).matches() and * Pattern.compile(pattern).matcher(key).replaceAll(replacement) calls. * *

Garbage Footprint

* * stringified allocates a new String for values that are not * of type String. *

* pattern and replacement incur pattern matcher allocation * costs. *

* Writing certain non-primitive values (e.g., BigDecimal, * Set, etc.) to JSON generates garbage, though most (e.g., * int, long, String, List, * boolean[], etc.) don't. * *

Examples

* * "$resolver" is left out in the following examples, since it is to be * defined by the actual resolver, e.g., {@link MapResolver}, * {@link ThreadContextDataResolver}. *

* Resolve the value of the field keyed with user:role: * *

 * {
 *   "$resolver": "…",
 *   "key": "user:role"
 * }
 * 
* * Resolve the string representation of the user:rank field value: * *
 * {
 *   "$resolver": "…",
 *   "key": "user:rank",
 *   "stringified": true
 * }
 * 
* * Resolve all fields into an object: * *
 * {
 *   "$resolver": "…"
 * }
 * 
* * Resolve all fields into an object such that values are converted to * string: * *
 * {
 *   "$resolver": "…",
 *   "stringified": true
 * }
 * 
* * Resolve all fields whose keys match with the user:(role|rank) regex * into an object: * *
 * {
 *   "$resolver": "…",
 *   "pattern": "user:(role|rank)"
 * }
 * 
* * Resolve all fields whose keys match with the user:(role|rank) regex * into an object after removing the user: prefix in the key: * *
 * {
 *   "$resolver": "…",
 *   "pattern": "user:(role|rank)",
 *   "replacement": "$1"
 * }
 * 
* * Merge all fields whose keys are matching with the * user:(role|rank) regex into the parent: * *
 * {
 *   "$resolver": "…",
 *   "flatten": true,
 *   "pattern": "user:(role|rank)"
 * }
 * 
* * After converting the corresponding field values to string, merge all fields * to parent such that keys are prefixed with _: * *
 * {
 *   "$resolver": "…",
 *   "stringified": true,
 *   "flatten": {
 *     "prefix": "_"
 *   }
 * }
 * 
* * @see MapResolver * @see ThreadContextDataResolver */ class ReadOnlyStringMapResolver implements EventResolver { private final EventResolver internalResolver; ReadOnlyStringMapResolver( final EventResolverContext context, final TemplateResolverConfig config, final Function mapAccessor) { this.internalResolver = createResolver(context, config, mapAccessor); } private static EventResolver createResolver( final EventResolverContext context, final TemplateResolverConfig config, final Function 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 prefix = config.getString(new String[] {"flatten", "prefix"}); final String key = config.getString("key"); if (key != null && flatten) { throw new IllegalArgumentException( "key and flatten options cannot be combined: " + config); } final String pattern = config.getString("pattern"); if (pattern != null && key != null) { throw new IllegalArgumentException( "pattern and key options cannot be combined: " + config); } final String replacement = config.getString("replacement"); if (pattern == null && replacement != null) { throw new IllegalArgumentException( "replacement cannot be provided without a pattern: " + config); } final boolean stringified = config.getBoolean("stringified", false); if (key != null) { return createKeyResolver(key, stringified, mapAccessor); } else { final RecyclerFactory recyclerFactory = context.getRecyclerFactory(); return createResolver( recyclerFactory, flatten, prefix, pattern, replacement, stringified, mapAccessor); } } private static EventResolver createKeyResolver( final String key, final boolean stringified, final Function 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 = String.valueOf(value); jsonWriter.writeString(valueString); } else { jsonWriter.writeValue(value); } } }; } private static EventResolver createResolver( final RecyclerFactory recyclerFactory, final boolean flatten, final String prefix, final String pattern, final String replacement, final boolean stringified, final Function mapAccessor) { // Compile the pattern. final Pattern compiledPattern = pattern == null ? null : Pattern.compile(pattern); // Create the recycler for the loop context. final Recycler loopContextRecycler = recyclerFactory.create(() -> { final LoopContext loopContext = new LoopContext(); if (prefix != null) { loopContext.prefix = prefix; loopContext.prefixedKey = new StringBuilder(prefix); } loopContext.pattern = compiledPattern; loopContext.replacement = replacement; loopContext.stringified = stringified; return loopContext; }); // Create the resolver. return createResolver(flatten, loopContextRecycler, mapAccessor); } private static EventResolver createResolver( final boolean flatten, final Recycler loopContextRecycler, final Function 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) { resolve(value, jsonWriter, false); } @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 String replacement; private boolean stringified; private JsonWriter jsonWriter; private int initJsonWriterStringBuilderLength; private boolean succeedingEntry; } private enum LoopMethod implements TriConsumer { INSTANCE; @Override public void accept( final String key, final Object value, final LoopContext loopContext) { final Matcher matcher = loopContext.pattern != null ? loopContext.pattern.matcher(key) : null; final boolean keyMatched = matcher == null || matcher.matches(); if (keyMatched) { final String replacedKey = matcher != null && loopContext.replacement != null ? matcher.replaceAll(loopContext.replacement) : key; final boolean succeedingEntry = loopContext.succeedingEntry || loopContext.initJsonWriterStringBuilderLength < loopContext.jsonWriter.getStringBuilder().length(); if (succeedingEntry) { loopContext.jsonWriter.writeSeparator(); } if (loopContext.prefix == null) { loopContext.jsonWriter.writeObjectKey(replacedKey); } else { loopContext.prefixedKey.setLength(loopContext.prefix.length()); loopContext.prefixedKey.append(replacedKey); loopContext.jsonWriter.writeObjectKey(loopContext.prefixedKey); } if (loopContext.stringified && !(value instanceof String)) { final String valueString = String.valueOf(value); loopContext.jsonWriter.writeString(valueString); } else { loopContext.jsonWriter.writeValue(value); } } } } @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); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy