com.sap.hcp.cf.log4j2.layout.JsonPatternLayout Maven / Gradle / Ivy
package com.sap.hcp.cf.log4j2.layout;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.StringLayout;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginConfiguration;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.AbstractStringLayout;
import org.apache.logging.log4j.core.util.StringBuilderWriter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.jr.ob.JSON;
import com.fasterxml.jackson.jr.ob.JSONComposer;
import com.fasterxml.jackson.jr.ob.comp.ArrayComposer;
import com.fasterxml.jackson.jr.ob.comp.ComposerBase;
import com.fasterxml.jackson.jr.ob.comp.ObjectComposer;
import com.sap.hcp.cf.log4j2.converter.api.Log4jContextFieldSupplier;
import com.sap.hcp.cf.log4j2.layout.supppliers.BaseFieldSupplier;
import com.sap.hcp.cf.log4j2.layout.supppliers.EventContextFieldSupplier;
import com.sap.hcp.cf.log4j2.layout.supppliers.LogEventUtilities;
import com.sap.hcp.cf.log4j2.layout.supppliers.RequestRecordFieldSupplier;
import com.sap.hcp.cf.logging.common.Fields;
import com.sap.hcp.cf.logging.common.converter.LineWriter;
import com.sap.hcp.cf.logging.common.converter.StacktraceLines;
import com.sap.hcp.cf.logging.common.serialization.ContextFieldConverter;
import com.sap.hcp.cf.logging.common.serialization.ContextFieldSupplier;
import com.sap.hcp.cf.logging.common.serialization.JsonSerializationException;
/**
* A {@link StringLayout} implementation that encodes an {@link LogEvent} as a
* JSON object.
*
* Under the hood, it's using Jackson to serialize the logging event into JSON.
* The encoder can be confiugred in the log4j.xml:
*
*
* <Appenders>
* <Console name="STDOUT-JSON" target="SYSTEM_OUT" follow="true">
* <JsonPatternLayout />
* </Console>
* </Appenders>
*
*
*
The layout can be customized by several xml elements. See the
* annotations on the factory method {@link #createLayout}.
*/
@Plugin(name = "JsonPatternLayout", category = "Core", elementType = "Layout", printObject = true)
public final class JsonPatternLayout extends AbstractStringLayout {
private static final String NEWLINE = "\n";
private List log4jContextFieldSuppliers = new ArrayList<>();
private List contextFieldSuppliers = new ArrayList<>();
private final ContextFieldConverter contextFieldConverter;
private int maxStacktraceSize;
private boolean sendDefaultValues;
private JSON json;
@PluginFactory
public static JsonPatternLayout createLayout(@PluginAttribute(value = "charset") final Charset charset,
@PluginAttribute(value = "sendDefaultValues") final boolean sendDefaultValues,
@PluginAttribute(value = "maxStacktraceSize") final int maxStacktraceSize,
@PluginAttribute(value = "jsonBuilder") final String jsonBuilderClass,
@PluginElement(value = "customField") CustomFieldElement[] customFieldMdcKeyNames,
@PluginElement(value = "log4jContextFieldSupplier") Log4jContextFieldSupplierElement[] log4jContextFieldSupplierElements,
@PluginElement(value = "contextFieldSupplier") ContextFieldSupplierElement[] contextFieldSupplierElements,
@PluginConfiguration final Configuration config) {
return new JsonPatternLayout(charset, sendDefaultValues, maxStacktraceSize, jsonBuilderClass,
customFieldMdcKeyNames, log4jContextFieldSupplierElements,
contextFieldSupplierElements);
}
protected JsonPatternLayout(Charset charset, boolean sendDefaultValues, int maxStacktraceSize,
String jsonBuilderClass, CustomFieldElement[] customFieldMdcKeys,
Log4jContextFieldSupplierElement[] log4jContextFieldSupplierElements,
ContextFieldSupplierElement[] contextFieldSupplierElements) {
super(charset);
this.sendDefaultValues = sendDefaultValues;
this.maxStacktraceSize = maxStacktraceSize > 0 ? maxStacktraceSize : 55 * 1024;
this.contextFieldConverter = contextFieldConverter(sendDefaultValues, customFieldMdcKeys);
this.json = createJson(jsonBuilderClass);
this.log4jContextFieldSuppliers.add(new BaseFieldSupplier());
this.log4jContextFieldSuppliers.add(new EventContextFieldSupplier());
this.log4jContextFieldSuppliers.add(new RequestRecordFieldSupplier());
if (log4jContextFieldSupplierElements != null) {
for (Log4jContextFieldSupplierElement current: log4jContextFieldSupplierElements) {
try {
Log4jContextFieldSupplier instance = createInstance(current.getSupplierClass(),
Log4jContextFieldSupplier.class);
log4jContextFieldSuppliers.add(instance);
} catch (ReflectiveOperationException cause) {
LOGGER.warn("Cannot register Log4jContextFieldSupplier.", cause);
}
}
}
if (contextFieldSupplierElements != null) {
for (ContextFieldSupplierElement current: contextFieldSupplierElements) {
try {
ContextFieldSupplier instance = createInstance(current.getSupplierClass(),
ContextFieldSupplier.class);
contextFieldSuppliers.add(instance);
} catch (ReflectiveOperationException cause) {
LOGGER.warn("Cannot register ContextFieldSupplier.", cause);
}
}
}
}
private static JSON createJson(String jsonBuilderClass) {
if (jsonBuilderClass == null || jsonBuilderClass.isEmpty()) {
return JSON.std;
}
try {
return createInstance(jsonBuilderClass, JSON.Builder.class).build();
} catch (ReflectiveOperationException cause) {
LOGGER.warn("Cannot register JsonBuilder, using default.", cause);
return JSON.std;
}
}
private static ContextFieldConverter contextFieldConverter(boolean sendDefaultValues,
CustomFieldElement... customFieldMdcKeys) {
List customFieldMdcKeyNames = new ArrayList<>(customFieldMdcKeys.length);
List retainFieldMdcKeyNames = new ArrayList<>(customFieldMdcKeys.length);
for (CustomFieldElement customField: customFieldMdcKeys) {
customFieldMdcKeyNames.add(customField.getKey());
if (customField.isRetainOriginal()) {
retainFieldMdcKeyNames.add(customField.getKey());
}
}
return new ContextFieldConverter(sendDefaultValues, customFieldMdcKeyNames, retainFieldMdcKeyNames);
}
private static T createInstance(String className, Class interfaceClass) throws ReflectiveOperationException {
ClassLoader classLoader = JsonPatternLayout.class.getClassLoader();
Class extends T> clazz = classLoader.loadClass(className).asSubclass(interfaceClass);
return clazz.getDeclaredConstructor().newInstance();
}
@Override
public String toSerializable(LogEvent event) {
try (StringBuilderWriter writer = new StringBuilderWriter(getStringBuilder())) {
ObjectComposer> oc = json.composeTo(writer).startObject();
addMarkers(oc, event);
Map contextFields = collectContextFields(event);
contextFieldConverter.addContextFields(oc, contextFields);
contextFieldConverter.addCustomFields(oc, contextFields);
addStacktrace(oc, event);
oc.end().finish();
return writer.append(NEWLINE).toString();
} catch (IOException | JsonSerializationException cause) {
// Fallback to emit just the formatted message
LOGGER.error("Conversion failed ", cause);
return LogEventUtilities.getFormattedMessage(event);
}
}
private void addMarkers(ObjectComposer
oc, LogEvent event) throws IOException,
JsonProcessingException {
if (sendDefaultValues || event.getMarker() != null) {
ArrayComposer> ac = oc.startArrayField(Fields.CATEGORIES);
addMarker(ac, event.getMarker());
ac.end();
}
}
private void addMarker(ArrayComposer
ac, org.apache.logging.log4j.Marker marker)
throws IOException {
if (marker == null) {
return;
}
ac.add(marker.getName());
if (marker.hasParents()) {
for (org.apache.logging.log4j.Marker current: marker.getParents()) {
addMarker(ac, current);
}
}
}
private Map collectContextFields(LogEvent event) {
Map contextFields = new HashMap<>();
contextFieldSuppliers.forEach(s -> contextFields.putAll(s.get()));
log4jContextFieldSuppliers.forEach(s -> contextFields.putAll(s.map(event)));
return contextFields;
}
private void addStacktrace(ObjectComposer
oc, LogEvent event) throws IOException,
JsonProcessingException {
if (event.getThrown() != null) {
LineWriter lw = new LineWriter();
event.getThrown().printStackTrace(new PrintWriter(lw));
List lines = lw.getLines();
StacktraceLines stacktraceLines = new StacktraceLines(lines);
ArrayComposer> ac = oc.startArrayField(Fields.STACKTRACE);
if (stacktraceLines.getTotalLineLength() <= maxStacktraceSize) {
for (String line: stacktraceLines.getLines()) {
ac.add(line);
}
} else {
ac.add("-------- STACK TRACE TRUNCATED --------");
for (String line: stacktraceLines.getFirstLines(maxStacktraceSize / 3)) {
ac.add(line);
}
ac.add("-------- OMITTED --------");
for (String line: stacktraceLines.getLastLines((maxStacktraceSize / 3) * 2)) {
ac.add(line);
}
}
ac.end();
}
}
@Override
public String getContentType() {
return "application/json";
}
}