diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/AbstractJacksonLayoutCopy.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/AbstractJacksonLayoutCopy.java new file mode 100644 index 000000000..b1fafe3b7 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/AbstractJacksonLayoutCopy.java @@ -0,0 +1,493 @@ +package software.amazon.lambda.powertools.logging.internal; + +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.impl.Log4jLogEvent; +import org.apache.logging.log4j.core.impl.ThrowableProxy; +import org.apache.logging.log4j.core.jackson.XmlConstants; +import org.apache.logging.log4j.core.layout.AbstractStringLayout; +import org.apache.logging.log4j.core.lookup.StrSubstitutor; +import org.apache.logging.log4j.core.time.Instant; +import org.apache.logging.log4j.core.util.KeyValuePair; +import org.apache.logging.log4j.core.util.StringBuilderWriter; +import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.util.ReadOnlyStringMap; +import org.apache.logging.log4j.util.Strings; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonRootName; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectWriter; + +abstract class AbstractJacksonLayoutCopy extends AbstractStringLayout { + + protected static final String DEFAULT_EOL = "\r\n"; + protected static final String COMPACT_EOL = Strings.EMPTY; + + public static abstract class Builder> extends AbstractStringLayout.Builder { + + @PluginBuilderAttribute + private boolean eventEol; + + @PluginBuilderAttribute + private String endOfLine; + + @PluginBuilderAttribute + private boolean compact; + + @PluginBuilderAttribute + private boolean complete; + + @PluginBuilderAttribute + private boolean locationInfo; + + @PluginBuilderAttribute + private boolean properties; + + @PluginBuilderAttribute + private boolean includeStacktrace = true; + + @PluginBuilderAttribute + private boolean stacktraceAsString = false; + + @PluginBuilderAttribute + private boolean includeNullDelimiter = false; + + @PluginBuilderAttribute + private boolean includeTimeMillis = false; + + @PluginElement("AdditionalField") + private KeyValuePair[] additionalFields; + + protected String toStringOrNull(final byte[] header) { + return header == null ? null : new String(header, Charset.defaultCharset()); + } + + public boolean getEventEol() { + return eventEol; + } + + public String getEndOfLine() { + return endOfLine; + } + + public boolean isCompact() { + return compact; + } + + public boolean isComplete() { + return complete; + } + + public boolean isLocationInfo() { + return locationInfo; + } + + public boolean isProperties() { + return properties; + } + + /** + * If "true", includes the stacktrace of any Throwable in the generated data, defaults to "true". + * @return If "true", includes the stacktrace of any Throwable in the generated data, defaults to "true". + */ + public boolean isIncludeStacktrace() { + return includeStacktrace; + } + + public boolean isStacktraceAsString() { + return stacktraceAsString; + } + + public boolean isIncludeNullDelimiter() { return includeNullDelimiter; } + + public boolean isIncludeTimeMillis() { + return includeTimeMillis; + } + + public KeyValuePair[] getAdditionalFields() { + return additionalFields; + } + + public B setEventEol(final boolean eventEol) { + this.eventEol = eventEol; + return asBuilder(); + } + + public B setEndOfLine(final String endOfLine) { + this.endOfLine = endOfLine; + return asBuilder(); + } + + public B setCompact(final boolean compact) { + this.compact = compact; + return asBuilder(); + } + + public B setComplete(final boolean complete) { + this.complete = complete; + return asBuilder(); + } + + public B setLocationInfo(final boolean locationInfo) { + this.locationInfo = locationInfo; + return asBuilder(); + } + + public B setProperties(final boolean properties) { + this.properties = properties; + return asBuilder(); + } + + /** + * If "true", includes the stacktrace of any Throwable in the generated JSON, defaults to "true". + * @param includeStacktrace If "true", includes the stacktrace of any Throwable in the generated JSON, defaults to "true". + * @return this builder + */ + public B setIncludeStacktrace(final boolean includeStacktrace) { + this.includeStacktrace = includeStacktrace; + return asBuilder(); + } + + /** + * Whether to format the stacktrace as a string, and not a nested object (optional, defaults to false). + * + * @return this builder + */ + public B setStacktraceAsString(final boolean stacktraceAsString) { + this.stacktraceAsString = stacktraceAsString; + return asBuilder(); + } + + /** + * Whether to include NULL byte as delimiter after each event (optional, default to false). + * + * @return this builder + */ + public B setIncludeNullDelimiter(final boolean includeNullDelimiter) { + this.includeNullDelimiter = includeNullDelimiter; + return asBuilder(); + } + + /** + * Whether to include the timestamp (in addition to the Instant) (optional, default to false). + * + * @return this builder + */ + public B setIncludeTimeMillis(final boolean includeTimeMillis) { + this.includeTimeMillis = includeTimeMillis; + return asBuilder(); + } + + /** + * Additional fields to set on each log event. + * + * @return this builder + */ + public B setAdditionalFields(final KeyValuePair[] additionalFields) { + this.additionalFields = additionalFields; + return asBuilder(); + } + } + + protected final String eol; + protected final ObjectWriter objectWriter; + protected final boolean compact; + protected final boolean complete; + protected final boolean includeNullDelimiter; + protected final ResolvableKeyValuePair[] additionalFields; + + @Deprecated + protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, final Charset charset, + final boolean compact, final boolean complete, final boolean eventEol, final Serializer headerSerializer, + final Serializer footerSerializer) { + this(config, objectWriter, charset, compact, complete, eventEol, headerSerializer, footerSerializer, false); + } + + @Deprecated + protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, final Charset charset, + final boolean compact, final boolean complete, final boolean eventEol, final Serializer headerSerializer, + final Serializer footerSerializer, final boolean includeNullDelimiter) { + this(config, objectWriter, charset, compact, complete, eventEol, null, headerSerializer, footerSerializer, includeNullDelimiter, null); + } + + protected AbstractJacksonLayoutCopy(final Configuration config, final ObjectWriter objectWriter, final Charset charset, + final boolean compact, final boolean complete, final boolean eventEol, final String endOfLine, final Serializer headerSerializer, + final Serializer footerSerializer, final boolean includeNullDelimiter, + final KeyValuePair[] additionalFields) { + super(config, charset, headerSerializer, footerSerializer); + this.objectWriter = objectWriter; + this.compact = compact; + this.complete = complete; + this.eol = endOfLine != null ? endOfLine : compact && !eventEol ? COMPACT_EOL : DEFAULT_EOL; + this.includeNullDelimiter = includeNullDelimiter; + this.additionalFields = prepareAdditionalFields(config, additionalFields); + } + + protected static boolean valueNeedsLookup(final String value) { + return value != null && value.contains("${"); + } + + private static ResolvableKeyValuePair[] prepareAdditionalFields(final Configuration config, final KeyValuePair[] additionalFields) { + if (additionalFields == null || additionalFields.length == 0) { + // No fields set + return ResolvableKeyValuePair.EMPTY_ARRAY; + } + + // Convert to specific class which already determines whether values needs lookup during serialization + final ResolvableKeyValuePair[] resolvableFields = new ResolvableKeyValuePair[additionalFields.length]; + + for (int i = 0; i < additionalFields.length; i++) { + final ResolvableKeyValuePair resolvable = resolvableFields[i] = new ResolvableKeyValuePair(additionalFields[i]); + + // Validate + if (config == null && resolvable.valueNeedsLookup) { + throw new IllegalArgumentException("configuration needs to be set when there are additional fields with variables"); + } + } + + return resolvableFields; + } + + /** + * Formats a {@link org.apache.logging.log4j.core.LogEvent}. + * + * @param event The LogEvent. + * @return The XML representation of the LogEvent. + */ + @Override + public String toSerializable(final LogEvent event) { + final StringBuilderWriter writer = new StringBuilderWriter(); + try { + toSerializable(event, writer); + return writer.toString(); + } catch (final IOException e) { + // Should this be an ISE or IAE? + LOGGER.error(e); + return Strings.EMPTY; + } + } + + private static LogEvent convertMutableToLog4jEvent(final LogEvent event) { + return event instanceof Log4jLogEvent ? event : Log4jLogEvent.createMemento(event); + } + + protected Object wrapLogEvent(final LogEvent event) { + if (additionalFields.length > 0) { + // Construct map for serialization - note that we are intentionally using original LogEvent + final Map additionalFieldsMap = resolveAdditionalFields(event); + // This class combines LogEvent with AdditionalFields during serialization + return new LogEventWithAdditionalFields(event, additionalFieldsMap); + } else if (event instanceof Message) { + // If the LogEvent implements the Messagee interface Jackson will not treat is as a LogEvent. + return new ReadOnlyLogEventWrapper(event); + } else { + // No additional fields, return original object + return event; + } + } + + private Map resolveAdditionalFields(final LogEvent logEvent) { + // Note: LinkedHashMap retains order + final Map additionalFieldsMap = new LinkedHashMap<>(additionalFields.length); + final StrSubstitutor strSubstitutor = configuration.getStrSubstitutor(); + + // Go over each field + for (final ResolvableKeyValuePair pair : additionalFields) { + if (pair.valueNeedsLookup) { + // Resolve value + additionalFieldsMap.put(pair.key, strSubstitutor.replace(logEvent, pair.value)); + } else { + // Plain text value + additionalFieldsMap.put(pair.key, pair.value); + } + } + + return additionalFieldsMap; + } + + public void toSerializable(final LogEvent event, final Writer writer) + throws JsonGenerationException, JsonMappingException, IOException { + objectWriter.writeValue(writer, wrapLogEvent(convertMutableToLog4jEvent(event))); + writer.write(eol); + if (includeNullDelimiter) { + writer.write('\0'); + } + markEvent(); + } + + @JsonRootName(XmlConstants.ELT_EVENT) + public static class LogEventWithAdditionalFields { + + private final Object logEvent; + private final Map additionalFields; + + public LogEventWithAdditionalFields(final Object logEvent, final Map additionalFields) { + this.logEvent = logEvent; + this.additionalFields = additionalFields; + } + + @JsonUnwrapped + public Object getLogEvent() { + return logEvent; + } + + @JsonAnyGetter + @SuppressWarnings("unused") + public Map getAdditionalFields() { + return additionalFields; + } + } + + protected static class ResolvableKeyValuePair { + + /** + * The empty array. + */ + static final ResolvableKeyValuePair[] EMPTY_ARRAY = {}; + + final String key; + final String value; + final boolean valueNeedsLookup; + + ResolvableKeyValuePair(final KeyValuePair pair) { + this.key = pair.getKey(); + this.value = pair.getValue(); + this.valueNeedsLookup = AbstractJacksonLayoutCopy.valueNeedsLookup(this.value); + } + } + + private static class ReadOnlyLogEventWrapper implements LogEvent { + + @JsonIgnore + private final LogEvent event; + + public ReadOnlyLogEventWrapper(LogEvent event) { + this.event = event; + } + + @Override + public LogEvent toImmutable() { + return event.toImmutable(); + } + + @Override + public Map getContextMap() { + return event.getContextMap(); + } + + @Override + public ReadOnlyStringMap getContextData() { + return event.getContextData(); + } + + @Override + public ThreadContext.ContextStack getContextStack() { + return event.getContextStack(); + } + + @Override + public String getLoggerFqcn() { + return event.getLoggerFqcn(); + } + + @Override + public Level getLevel() { + return event.getLevel(); + } + + @Override + public String getLoggerName() { + return event.getLoggerName(); + } + + @Override + public Marker getMarker() { + return event.getMarker(); + } + + @Override + public Message getMessage() { + return event.getMessage(); + } + + @Override + public long getTimeMillis() { + return event.getTimeMillis(); + } + + @Override + public Instant getInstant() { + return event.getInstant(); + } + + @Override + public StackTraceElement getSource() { + return event.getSource(); + } + + @Override + public String getThreadName() { + return event.getThreadName(); + } + + @Override + public long getThreadId() { + return event.getThreadId(); + } + + @Override + public int getThreadPriority() { + return event.getThreadPriority(); + } + + @Override + public Throwable getThrown() { + return event.getThrown(); + } + + @Override + public ThrowableProxy getThrownProxy() { + return event.getThrownProxy(); + } + + @Override + public boolean isEndOfBatch() { + return event.isEndOfBatch(); + } + + @Override + public boolean isIncludeLocation() { + return event.isIncludeLocation(); + } + + @Override + public void setEndOfBatch(boolean endOfBatch) { + + } + + @Override + public void setIncludeLocation(boolean locationRequired) { + + } + + @Override + public long getNanoTime() { + return event.getNanoTime(); + } + } +} \ No newline at end of file diff --git a/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JacksonFactoryCopy.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JacksonFactoryCopy.java new file mode 100644 index 000000000..67ec01058 --- /dev/null +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/JacksonFactoryCopy.java @@ -0,0 +1,116 @@ +package software.amazon.lambda.powertools.logging.internal; + +import com.fasterxml.jackson.core.PrettyPrinter; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.core.util.MinimalPrettyPrinter; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; +import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; +import org.apache.logging.log4j.core.impl.Log4jLogEvent; +import org.apache.logging.log4j.core.jackson.JsonConstants; +import org.apache.logging.log4j.core.jackson.Log4jJsonObjectMapper; + +import java.util.HashSet; +import java.util.Set; + +abstract class JacksonFactoryCopy { + + static class JSON extends JacksonFactoryCopy { + + private final boolean encodeThreadContextAsList; + private final boolean includeStacktrace; + private final boolean stacktraceAsString; + private final boolean objectMessageAsJsonObject; + + public JSON(final boolean encodeThreadContextAsList, final boolean includeStacktrace, final boolean stacktraceAsString, final boolean objectMessageAsJsonObject) { + this.encodeThreadContextAsList = encodeThreadContextAsList; + this.includeStacktrace = includeStacktrace; + this.stacktraceAsString = stacktraceAsString; + this.objectMessageAsJsonObject = objectMessageAsJsonObject; + } + + @Override + protected String getPropertNameForContextMap() { + return JsonConstants.ELT_CONTEXT_MAP; + } + + @Override + protected String getPropertyNameForTimeMillis() { + return JsonConstants.ELT_TIME_MILLIS; + } + + @Override + protected String getPropertyNameForInstant() { + return JsonConstants.ELT_INSTANT; + } + + @Override + protected String getPropertNameForSource() { + return JsonConstants.ELT_SOURCE; + } + + @Override + protected String getPropertNameForNanoTime() { + return JsonConstants.ELT_NANO_TIME; + } + + @Override + protected PrettyPrinter newCompactPrinter() { + return new MinimalPrettyPrinter(); + } + + @Override + protected ObjectMapper newObjectMapper() { + return new Log4jJsonObjectMapper(encodeThreadContextAsList, includeStacktrace, stacktraceAsString, objectMessageAsJsonObject); + } + + @Override + protected PrettyPrinter newPrettyPrinter() { + return new DefaultPrettyPrinter(); + } + + } + + abstract protected String getPropertyNameForTimeMillis(); + + abstract protected String getPropertyNameForInstant(); + + abstract protected String getPropertNameForContextMap(); + + abstract protected String getPropertNameForSource(); + + abstract protected String getPropertNameForNanoTime(); + + abstract protected PrettyPrinter newCompactPrinter(); + + abstract protected ObjectMapper newObjectMapper(); + + abstract protected PrettyPrinter newPrettyPrinter(); + + ObjectWriter newWriter(final boolean locationInfo, final boolean properties, final boolean compact) { + return newWriter(locationInfo, properties, compact, false); + } + + ObjectWriter newWriter(final boolean locationInfo, final boolean properties, final boolean compact, + final boolean includeMillis) { + final SimpleFilterProvider filters = new SimpleFilterProvider(); + final Set except = new HashSet<>(3); + if (!locationInfo) { + except.add(this.getPropertNameForSource()); + } + if (!properties) { + except.add(this.getPropertNameForContextMap()); + } + if (includeMillis) { + except.add(getPropertyNameForInstant()); + } else { + except.add(getPropertyNameForTimeMillis()); + } + except.add(this.getPropertNameForNanoTime()); + filters.addFilter(Log4jLogEvent.class.getName(), SimpleBeanPropertyFilter.serializeAllExcept(except)); + final ObjectWriter writer = this.newObjectMapper().writer(compact ? this.newCompactPrinter() : this.newPrettyPrinter()); + return writer.with(filters); + } + +} \ No newline at end of file diff --git a/powertools-logging/src/main/java/org/apache/logging/log4j/core/layout/LambdaJsonLayout.java b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonLayout.java similarity index 95% rename from powertools-logging/src/main/java/org/apache/logging/log4j/core/layout/LambdaJsonLayout.java rename to powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonLayout.java index 49e233456..ad7ca3cf7 100644 --- a/powertools-logging/src/main/java/org/apache/logging/log4j/core/layout/LambdaJsonLayout.java +++ b/powertools-logging/src/main/java/software/amazon/lambda/powertools/logging/internal/LambdaJsonLayout.java @@ -11,7 +11,7 @@ * limitations under the License. * */ -package org.apache.logging.log4j.core.layout; +package software.amazon.lambda.powertools.logging.internal; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonGetter; @@ -26,6 +26,7 @@ import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; import org.apache.logging.log4j.core.jackson.XmlConstants; +import org.apache.logging.log4j.core.layout.PatternLayout; import org.apache.logging.log4j.core.util.KeyValuePair; import org.apache.logging.log4j.util.Strings; @@ -43,14 +44,14 @@ import static java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME; @Plugin(name = "LambdaJsonLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) -public class LambdaJsonLayout extends AbstractJacksonLayout { +public final class LambdaJsonLayout extends AbstractJacksonLayoutCopy { private static final String DEFAULT_FOOTER = "]"; private static final String DEFAULT_HEADER = "["; static final String CONTENT_TYPE = "application/json"; - public static class Builder> extends AbstractJacksonLayout.Builder + public static class Builder> extends AbstractJacksonLayoutCopy.Builder implements org.apache.logging.log4j.core.util.Builder { @PluginBuilderAttribute @@ -101,7 +102,7 @@ private LambdaJsonLayout(final Configuration config, final boolean locationInfo, final boolean includeStacktrace, final boolean stacktraceAsString, final boolean includeNullDelimiter, final KeyValuePair[] additionalFields, final boolean objectMessageAsJsonObject) { - super(config, new JacksonFactory.JSON(encodeThreadContextAsList, includeStacktrace, stacktraceAsString, objectMessageAsJsonObject).newWriter( + super(config, new JacksonFactoryCopy.JSON(encodeThreadContextAsList, includeStacktrace, stacktraceAsString, objectMessageAsJsonObject).newWriter( locationInfo, properties, compact), charset, compact, complete, eventEol, null, diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index e695069a7..6ff64c89b 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -13,11 +13,19 @@ - + - + + + + + + + + + @@ -26,11 +34,19 @@ - + - + + + + + + + + +