/** * Copyright (c) 2015 GoDaddy * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.godaddy.logging; import com.esotericsoftware.reflectasm.FieldAccess; import com.esotericsoftware.reflectasm.MethodAccess; import com.google.common.collect.Lists; import com.google.common.primitives.Primitives; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; public abstract class LoggerMessageBuilder<T> implements MessageBuilder<T> { private List<Object> processedObjects = new LinkedList<>(); protected LoggingConfigs configs; protected Integer currentRecursiveLevel = 0; public LoggerMessageBuilder(LoggingConfigs configs) { this.configs = configs; } public LoggerMessageBuilder(LoggingConfigs configs, Integer currentRecursiveLevel) { this.configs = configs; this.currentRecursiveLevel = currentRecursiveLevel; } @Override public abstract RunningLogContext<T> buildMessage(final LogContext<T> previous, final Object currentObject); protected void buildMessage(Object obj, List<String> path, String currentField) { if (currentRecursiveLevel > configs.getRecursiveLevel()) { return; } /** * If the custom mapper contains a key that is assignable from obj.getClass() then the function related to the value of the * custom mappers key is applied and appended to the builder. */ if (processedCustom(obj, currentField)) { return; } /** If the object is null "=<null>" is appended to show that the object was null in the logs. */ if (obj == null) { processNull(currentField); } else if (obj instanceof LogMessage) { processLogMessage((LogMessage) obj); } /** If the object is an instance of collection, only the size of the collection is logged. */ else if (obj instanceof Collection<?>) { processCollection(currentField, (Collection) obj); } else if (obj.getClass().isArray()) { processArray(currentField, obj); } else if (obj instanceof Map) { processMap(currentField, (Map) obj); } /** If the object is an instance of String, the String is wrapped in quotes. */ else if (obj instanceof String) { processString(currentField, (String) obj); } else if (Primitives.isWrapperType(obj.getClass())) { processPrimitive(currentField, obj); } else if (obj instanceof Enum) { processEnum(currentField, obj); } else { processObject(obj, path, currentField); } } protected abstract void processNull(String currentField); protected abstract void processLogMessage(LogMessage logMessage); protected abstract void processCollection(String currentField, Collection collection); protected abstract void processArray(String currentField, Object array); protected abstract void processMap(String currentField, Map map); protected abstract void processString(String currentField, String str); protected abstract void processPrimitive(String currentField, Object obj); protected abstract void processEnum(String currentField, Object obj); protected boolean processObject(Object obj, List<String> path, String currentField) { if (cyclesDetected(obj)) { return false; } markObjectAsProcessed(obj); currentRecursiveLevel++; recurseThroughObject(obj, path, currentField); return true; } private void markObjectAsProcessed(final Object obj) { processedObjects.add(obj); } private boolean cyclesDetected(final Object obj) { for (Object cached : processedObjects) { // use reference equality if (cached == obj) { return true; } } return false; } protected boolean processedCustom(Object obj, String currentField) { if (configs.getCustomMapper() == null || obj == null || obj instanceof LogMessage) { return false; } final Optional<Class<?>> customMap = configs.getCustomMapper() .keySet() .stream() .filter(i -> i.isAssignableFrom(obj.getClass())) .findFirst(); if (!customMap.isPresent()) { return false; } processCustomImpl(currentField, configs.getCustomMapper() .get(customMap.get()) .apply(obj)); return true; } protected abstract void processCustomImpl(String currentField, String message); protected String trimMethodOfPrefix(String methodName) { for (String startsWith : configs.getMethodPrefixes()) { if (methodName.startsWith(startsWith)) { return methodName.length() == startsWith.length() ? methodName : Character.toLowerCase(methodName.charAt(startsWith.length())) + methodName.substring(startsWith.length() + 1); } } return null; } protected String formatMethod(List<String> path, String currentField) { currentField = trimMethodOfPrefix(currentField); StringBuilder pathBuilder = new StringBuilder(); path.stream().forEach(p -> pathBuilder.append(p).append(".")); pathBuilder.append(currentField); path.add(currentField); return pathBuilder.toString(); } /** * Retrieves the objects methods and fields. Recurses through the objects methods. * * @param obj - Current object being recursed through. * @param currentField - Recursive prefix. * @param recursiveLevel - Current level in recursion. maximum amount of recursion levels is defined by * RECURSIVE_LEVEL. */ private void recurseThroughObject(Object obj, List<String> path, String currentField) { MethodAccess methodAccess = MethodAccess.get(obj.getClass()); for (LogCache logCache : CacheableAccessors.getMethodIndexes(obj.getClass(), methodAccess)) { if (canLogMethod(logCache, methodAccess)) { List<String> recursivePath = Lists.newArrayList(path); Object logResult; try { logResult = methodAccess.invoke(obj, logCache.getIndex()); } catch(IllegalAccessError er) { logResult = "<Illegal Method Access Error>"; } catch (Throwable t) { logResult = configs.getExceptionTranslator().translate(t); } try { buildMessage(getLogMessage(logCache, logResult), recursivePath, formatMethod(recursivePath, methodAccess.getMethodNames()[logCache.getIndex()])); } catch (Throwable t) { // result is ignored, but can be captured for debugging since we've already tried to catch // and build configs.getExceptionTranslator().translate(t); } } } FieldAccess fieldAccess = FieldAccess.get(obj.getClass()); for (LogCache logCache : CacheableAccessors.getFieldIndexes(obj.getClass(), fieldAccess)) { String fieldName = "???"; try { if (Scope.SKIP == logCache.getLogScope()) { continue; } fieldName = fieldAccess.getFieldNames()[logCache.getIndex()]; List<String> recursivePath = Lists.newArrayList(path); recursivePath.add(fieldName); if (!configs.getExcludesPrefixes().stream().anyMatch(fieldName::startsWith)) { buildMessage(getLogMessage(logCache, fieldAccess.get(obj, logCache.getIndex())), recursivePath, formatField(currentField, fieldName)); } } catch (Throwable t) { String fieldError = configs.getExceptionTranslator().translate(t); buildMessage(getLogMessage(logCache, fieldError), path, formatField(currentField, fieldName)); } } } protected String formatField(String currentField, String fieldName) { return currentField.isEmpty() ? fieldName : currentField + "." + fieldName; } private boolean canLogMethod(LogCache logCache, MethodAccess methodAccess) { boolean logScopeSkip = Scope.SKIP == logCache.getLogScope(); boolean returnTypeNotVoid = methodAccess.getReturnTypes()[logCache.getIndex()] != void.class; boolean methodHasNoParameters = methodAccess.getParameterTypes()[logCache.getIndex()].length == 0; boolean methodStartsWithDefinedPrefix = trimMethodOfPrefix(methodAccess.getMethodNames()[logCache.getIndex()]) != null; return !logScopeSkip && returnTypeNotVoid && methodHasNoParameters && methodStartsWithDefinedPrefix; } private Object getLogMessage(LogCache logCache, Object object) { if (logCache.getLogScope() == Scope.HASH) { try { return configs.getHashProcessor().process(object); } catch (Throwable t) { return configs.getExceptionTranslator().translate(t); } } return object; } }