
com.sap.hcp.cf.logback.encoder.JsonEncoder Maven / Gradle / Ivy
package com.sap.hcp.cf.logback.encoder;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.ThrowableProxy;
import com.sap.hcp.cf.logging.common.converter.LineWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.jr.ob.JSON;
import com.fasterxml.jackson.jr.ob.JSON.Builder;
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.logback.converter.api.LogbackContextFieldSupplier;
import com.sap.hcp.cf.logging.common.Fields;
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;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.StackTraceElementProxy;
import ch.qos.logback.core.encoder.Encoder;
import ch.qos.logback.core.encoder.EncoderBase;
/**
* An {@link Encoder} implementation that encodes an {@link ILoggingEvent} 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 logback.xml:
*
*
* <appender name="STDOUT-JSON" class="ch.qos.logback.core.ConsoleAppender">
* <encoder class="com.sap.hcp.cf.logback.encoder.JsonEncoder"/>
* </appender>
*
*
*
The encoder can be customized by several xml elements. See the
* Javadoc on the setter methods of this class.
*/
public class JsonEncoder extends EncoderBase {
private static final String NEWLINE = "\n";
private Charset charset = StandardCharsets.UTF_8;
private List customFieldMdcKeyNames = new ArrayList<>();
private List retainFieldMdcKeyNames = new ArrayList<>();
private boolean sendDefaultValues = false;
private List logbackContextFieldSuppliers = new ArrayList<>();
private List contextFieldSuppliers = new ArrayList<>();
private int maxStacktraceSize = 55 * 1024;
private JSON.Builder jsonBuilder = JSON.builder();
private JSON json;
private ContextFieldConverter contextFieldConverter;
public JsonEncoder() {
logbackContextFieldSuppliers.add(new BaseFieldSupplier());
logbackContextFieldSuppliers.add(new EventContextFieldSupplier());
logbackContextFieldSuppliers.add(new RequestRecordFieldSupplier());
}
/**
*
* Adds a field to the "#cf" object in the generated output. If the log
* event contains this field, it will be put into this nested object. It
* will not appear on top level in the generated JSON output, unless it is
* also added as retained field. See
* {@link #addRetainFieldMdcKeyName(String)}.
*
*
* This method is called by Joran for the xml tag
* {@code } in the logback.xml configuration file.
*
*
* @param name
* the field name (key) to add as custom field
*/
public void addCustomFieldMdcKeyName(String name) {
customFieldMdcKeyNames.add(name);
}
/**
*
* Retains a copy of a custom field added by
* {@link #addCustomFieldMdcKeyName(String)} at top level in the generated
* JSON output. This is useful when sending logs to different backend
* systems, that expect custom fields at different structures.
*
*
* This method is called by Joran for the xml tag
* {@code } in the logback.xml configuration file.
*
*
* @param name
* the field name (key) to add as custom field
*/
public void addRetainFieldMdcKeyName(String name) {
retainFieldMdcKeyNames.add(name);
}
/**
*
* Use the given charset for message creation. Defaults to utf8. Note, that
* Jackson is using utf8 by default independently from this configuration.
* You need to change the JSON build with {@link #setJsonBuilder(String)} to
* change the Jackson encoding.
*
*
* This method is called by Joran for the xml tag {@code } in the
* logback.xml configuration file.
*
*
* @param name
* the name of the charset to use
*/
public void setCharset(String name) {
try {
this.charset = Charset.forName(name);
} catch (IllegalArgumentException cause) {
LoggerHolder.LOG.warn("Cannot set charset ''" + name + "''. Falling back to default.", cause);
}
}
/**
*
* Limit stacktraces to this number of characters. This is to prevent
* message sizes above the platform limit. The default value is 55*1024. It
* should result in messages below 64k in size. The size should be adjusted
* to fit the limit and the used fields of the messages. Stacktraces, that
* exceed the size will be shortened in the middle. Top and bottom most
* lines will be retained.
*
*
* This method is called by Joran for the xml tag
* {@code } in the logback.xml configuration file.
*
*
* @param maxStacktraceSize
* the maximum number of characters to be allowed for stacktraces
*/
public void setMaxStacktraceSize(int maxStacktraceSize) {
this.maxStacktraceSize = maxStacktraceSize;
}
/**
*
* Send default values. Fields with empty or default values, e.g. "-" for
* strings will not be added to the log message. This behaviour can be
* changed to always emit all fields.
*
*
* This method is called by Joran for the xml tag
* {@code } in the logback.xml configuration file.
*
*
* @param sendDefaultValues
* the maximum number of characters to be allowed for stacktraces
*/
public void setSendDefaultValues(boolean sendDefaultValues) {
this.sendDefaultValues = sendDefaultValues;
}
/**
*
* Overwrites the default JsonBuilder with the given class. This can be used
* to modify the created JSON, e.g. with custom escaping or encoding.
*
*
* This method is called by Joran for the xml tag {@code } in
* the logback.xml configuration file.
*
*
* @param className
* the maximum number of characters to be allowed for stacktraces
*/
public void setJsonBuilder(String className) {
try {
Builder builder = createInstance(className, JSON.Builder.class);
this.jsonBuilder = builder;
} catch (Exception cause) {
LoggerHolder.LOG.warn("Cannot register JsonBuilder, using default.", cause);
}
}
private T createInstance(String className, Class interfaceClass) throws InstantiationException,
IllegalAccessException,
IllegalArgumentException,
InvocationTargetException,
NoSuchMethodException, SecurityException,
ClassNotFoundException {
ClassLoader classLoader = this.getClass().getClassLoader();
Class extends T> clazz = classLoader.loadClass(className).asSubclass(interfaceClass);
return clazz.getDeclaredConstructor().newInstance();
}
/**
*
* Use this class to extract context fields from the logback Logevents. The
* provided classes are applied in order. Later instances can overwrite
* earlier generated fields. LogbackContextFieldSuppliers are executed after
* ContextFieldSuppliers.
*
*
* This method is called by Joran for the xml tag
* {@code } in the logback.xml configuration
* file.
*
*
* @param className
* the maximum number of characters to be allowed for stacktraces
*/
public void addLogbackContextFieldSupplier(String className) {
try {
LogbackContextFieldSupplier instance = createInstance(className, LogbackContextFieldSupplier.class);
logbackContextFieldSuppliers.add(instance);
} catch (Exception cause) {
LoggerHolder.LOG.warn("Cannot register LogbackContextFieldSupplier.", cause);
}
}
/**
*
* Use this class to to provide additional context fields, e.g. from the
* environment. The provided classes are applied in order. Later instances
* can overwrite earlier generated fields. LogbackContextFieldSuppliers are
* executed after ContextFieldSuppliers.
*
*
* This method is called by Joran for the xml tag
* {@code } in the logback.xml configuration file.
*
*
* @param className
* the maximum number of characters to be allowed for stacktraces
*/
public void addContextFieldSupplier(String className) {
try {
ContextFieldSupplier instance = createInstance(className, ContextFieldSupplier.class);
contextFieldSuppliers.add((ContextFieldSupplier) instance);
} catch (Exception cause) {
LoggerHolder.LOG.warn("Cannot register ContextFieldSupplier.", cause);
}
}
@Override
public void start() {
this.json = new JSON(jsonBuilder);
this.contextFieldConverter = new ContextFieldConverter(sendDefaultValues, customFieldMdcKeyNames,
retainFieldMdcKeyNames);
super.start();
}
@Override
public byte[] headerBytes() {
return null;
}
@Override
public byte[] footerBytes() {
return null;
}
@Override
public byte[] encode(ILoggingEvent event) {
return getJson(event).getBytes(charset);
}
private String getJson(ILoggingEvent event) {
try (StringWriter writer = new StringWriter()) {
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 ex) {
// Fallback to emit just the message
LoggerHolder.LOG.error("Conversion failed ", ex);
return event.getFormattedMessage() + NEWLINE;
}
}
private void addMarkers(ObjectComposer
oc, ILoggingEvent 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, Marker parent) throws IOException {
if (parent == null) {
return;
}
if (parent.hasReferences()) {
Iterator current = parent.iterator();
while (current.hasNext()) {
addMarker(ac, current.next());
}
}
ac.add(parent.getName());
}
private Map collectContextFields(ILoggingEvent event) {
Map contextFields = new HashMap<>();
contextFieldSuppliers.forEach(s -> contextFields.putAll(s.get()));
logbackContextFieldSuppliers.forEach(s -> contextFields.putAll(s.map(event)));
return contextFields;
}
private void addStacktrace(ObjectComposer
oc, ILoggingEvent event) throws IOException,
JsonProcessingException {
IThrowableProxy proxy = event.getThrowableProxy();
if (proxy != null && ThrowableProxy.class.isAssignableFrom(proxy.getClass())) {
Throwable throwable = ((ThrowableProxy) proxy).getThrowable();
LineWriter lw = new LineWriter();
throwable.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();
}
}
private static class LoggerHolder {
static final Logger LOG = LoggerFactory.getLogger(LoggerHolder.class.getEnclosingClass());
}
}