From 7473be10fe31b616dd85bbea138da37b3bce0383 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 22 Apr 2024 15:04:47 +0200 Subject: [PATCH] add option to encode Throwable in a raw format for easy ingestion into logging backends Signed-off-by: Gregor Zeitlinger --- .../encoder/ArrayThrowableEncoder.java | 89 ++++++++++++ .../logback/classic/encoder/JsonEncoder.java | 134 +++--------------- .../PlainStackTraceThrowableEncoder.java | 35 +++++ .../classic/encoder/ThrowableEncoder.java | 66 +++++++++ .../classic/encoder/JsonEncoderTest.java | 20 ++- 5 files changed, 229 insertions(+), 115 deletions(-) create mode 100644 logback-classic/src/main/java/ch/qos/logback/classic/encoder/ArrayThrowableEncoder.java create mode 100644 logback-classic/src/main/java/ch/qos/logback/classic/encoder/PlainStackTraceThrowableEncoder.java create mode 100644 logback-classic/src/main/java/ch/qos/logback/classic/encoder/ThrowableEncoder.java diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/encoder/ArrayThrowableEncoder.java b/logback-classic/src/main/java/ch/qos/logback/classic/encoder/ArrayThrowableEncoder.java new file mode 100644 index 0000000000..68e85e0bef --- /dev/null +++ b/logback-classic/src/main/java/ch/qos/logback/classic/encoder/ArrayThrowableEncoder.java @@ -0,0 +1,89 @@ +package ch.qos.logback.classic.encoder; + +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.StackTraceElementProxy; + +import static ch.qos.logback.classic.encoder.JsonEncoder.CAUSE_ATTR_NAME; +import static ch.qos.logback.classic.encoder.JsonEncoder.CLASS_NAME_ATTR_NAME; +import static ch.qos.logback.classic.encoder.JsonEncoder.CLOSE_ARRAY; +import static ch.qos.logback.classic.encoder.JsonEncoder.CLOSE_OBJ; +import static ch.qos.logback.classic.encoder.JsonEncoder.METHOD_NAME_ATTR_NAME; +import static ch.qos.logback.classic.encoder.JsonEncoder.OPEN_ARRAY; +import static ch.qos.logback.classic.encoder.JsonEncoder.OPEN_OBJ; +import static ch.qos.logback.classic.encoder.JsonEncoder.QUOTE; +import static ch.qos.logback.classic.encoder.JsonEncoder.QUOTE_COL; +import static ch.qos.logback.classic.encoder.JsonEncoder.STEP_ARRAY_NAME_ATTRIBUTE; +import static ch.qos.logback.classic.encoder.JsonEncoder.SUPPRESSED_ATTR_NAME; +import static ch.qos.logback.classic.encoder.JsonEncoder.VALUE_SEPARATOR; +import static ch.qos.logback.classic.encoder.JsonEncoder.appenderMember; +import static ch.qos.logback.classic.encoder.JsonEncoder.appenderMemberWithIntValue; +import static ch.qos.logback.classic.encoder.JsonEncoder.nullSafeStr; + +class ArrayThrowableEncoder extends ThrowableEncoder { + + private static final String FILE_NAME_ATTR_NAME = "fileName"; + private static final String LINE_NUMBER_ATTR_NAME = "lineNumber"; + + @Override + void encodeStackTrace(StringBuilder sb, IThrowableProxy itp) { + appendSTEPArray(sb, itp.getStackTraceElementProxyArray(), itp.getCommonFrames()); + + IThrowableProxy cause = itp.getCause(); + if (cause != null) { + sb.append(VALUE_SEPARATOR); + appendThrowableProxy(sb, CAUSE_ATTR_NAME, cause); + } + + IThrowableProxy[] suppressedArray = itp.getSuppressed(); + if (suppressedArray != null && suppressedArray.length != 0) { + sb.append(VALUE_SEPARATOR); + sb.append(QUOTE).append(SUPPRESSED_ATTR_NAME).append(QUOTE_COL); + sb.append(OPEN_ARRAY); + boolean first = true; + for (IThrowableProxy suppressedITP : suppressedArray) { + if (first) { + first = false; + } else { + sb.append(VALUE_SEPARATOR); + } + appendThrowableProxy(sb, null, suppressedITP); + } + sb.append(CLOSE_ARRAY); + } + } + + private void appendSTEPArray(StringBuilder sb, StackTraceElementProxy[] stepArray, int commonFrames) { + sb.append(QUOTE).append(STEP_ARRAY_NAME_ATTRIBUTE).append(QUOTE_COL).append(OPEN_ARRAY); + + int len = stepArray != null ? stepArray.length : 0; + + if (commonFrames >= len) { + commonFrames = 0; + } + + for (int i = 0; i < len - commonFrames; i++) { + if (i != 0) + sb.append(VALUE_SEPARATOR); + + StackTraceElementProxy step = stepArray[i]; + + sb.append(OPEN_OBJ); + StackTraceElement ste = step.getStackTraceElement(); + + appenderMember(sb, CLASS_NAME_ATTR_NAME, nullSafeStr(ste.getClassName())); + sb.append(VALUE_SEPARATOR); + + appenderMember(sb, METHOD_NAME_ATTR_NAME, nullSafeStr(ste.getMethodName())); + sb.append(VALUE_SEPARATOR); + + appenderMember(sb, FILE_NAME_ATTR_NAME, nullSafeStr(ste.getFileName())); + sb.append(VALUE_SEPARATOR); + + appenderMemberWithIntValue(sb, LINE_NUMBER_ATTR_NAME, ste.getLineNumber()); + sb.append(CLOSE_OBJ); + + } + + sb.append(CLOSE_ARRAY); + } +} diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder.java b/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder.java index f4d00985cc..7f84e04ba5 100644 --- a/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder.java +++ b/logback-classic/src/main/java/ch/qos/logback/classic/encoder/JsonEncoder.java @@ -15,9 +15,7 @@ package ch.qos.logback.classic.encoder; import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.classic.spi.IThrowableProxy; import ch.qos.logback.classic.spi.LoggerContextVO; -import ch.qos.logback.classic.spi.StackTraceElementProxy; import ch.qos.logback.core.CoreConstants; import ch.qos.logback.core.encoder.EncoderBase; import org.slf4j.Marker; @@ -73,8 +71,6 @@ public class JsonEncoder extends EncoderBase { public static final String THROWABLE_ATTR_NAME = "throwable"; - private static final String CYCLIC_THROWABLE_ATTR_NAME = "cyclic"; - public static final String CAUSE_ATTR_NAME = "cause"; public static final String SUPPRESSED_ATTR_NAME = "suppressed"; @@ -83,25 +79,23 @@ public class JsonEncoder extends EncoderBase { public static final String CLASS_NAME_ATTR_NAME = "className"; public static final String METHOD_NAME_ATTR_NAME = "methodName"; - private static final String FILE_NAME_ATTR_NAME = "fileName"; - private static final String LINE_NUMBER_ATTR_NAME = "lineNumber"; public static final String STEP_ARRAY_NAME_ATTRIBUTE = "stepArray"; - private static final char OPEN_OBJ = '{'; - private static final char CLOSE_OBJ = '}'; - private static final char OPEN_ARRAY = '['; - private static final char CLOSE_ARRAY = ']'; + static final char OPEN_OBJ = '{'; + static final char CLOSE_OBJ = '}'; + static final char OPEN_ARRAY = '['; + static final char CLOSE_ARRAY = ']'; - private static final char QUOTE = DOUBLE_QUOTE_CHAR; + static final char QUOTE = DOUBLE_QUOTE_CHAR; private static final char SP = ' '; private static final char ENTRY_SEPARATOR = COLON_CHAR; private static final String COL_SP = ": "; - private static final String QUOTE_COL = "\":"; + static final String QUOTE_COL = "\":"; - private static final char VALUE_SEPARATOR = COMMA_CHAR; + static final char VALUE_SEPARATOR = COMMA_CHAR; private boolean withSequenceNumber = true; @@ -119,6 +113,7 @@ public class JsonEncoder extends EncoderBase { private boolean withMessage = true; private boolean withArguments = true; private boolean withThrowable = true; + private boolean withPlainStackTrace = false; private boolean withFormattedMessage = false; @@ -191,7 +186,7 @@ public byte[] encode(ILoggingEvent event) { appendArgumentArray(sb, event); if (withThrowable) - appendThrowableProxy(sb, THROWABLE_ATTR_NAME, event.getThrowableProxy()); + ThrowableEncoder.appendThrowableProxy(sb, event.getThrowableProxy(), withPlainStackTrace); sb.append(CLOSE_OBJ); sb.append(CoreConstants.JSON_LINE_SEPARATOR); @@ -239,105 +234,11 @@ private void appendMap(StringBuilder sb, String attrName, Map ma sb.append(CLOSE_OBJ); } - private void appendThrowableProxy(StringBuilder sb, String attributeName, IThrowableProxy itp) { - - // in the nominal case, attributeName != null. However, attributeName will be null for suppressed - // IThrowableProxy array, in which case no attribute name is needed - if (attributeName != null) { - sb.append(QUOTE).append(attributeName).append(QUOTE_COL); - if (itp == null) { - sb.append(NULL_STR); - return; - } - } - - sb.append(OPEN_OBJ); - - appenderMember(sb, CLASS_NAME_ATTR_NAME, nullSafeStr(itp.getClassName())); - - sb.append(VALUE_SEPARATOR); - appenderMember(sb, MESSAGE_ATTR_NAME, jsonEscape(itp.getMessage())); - - if (itp.isCyclic()) { - sb.append(VALUE_SEPARATOR); - appenderMember(sb, CYCLIC_THROWABLE_ATTR_NAME, jsonEscape("true")); - } - - sb.append(VALUE_SEPARATOR); - appendSTEPArray(sb, itp.getStackTraceElementProxyArray(), itp.getCommonFrames()); - - if (itp.getCommonFrames() != 0) { - sb.append(VALUE_SEPARATOR); - appenderMemberWithIntValue(sb, COMMON_FRAMES_COUNT_ATTR_NAME, itp.getCommonFrames()); - } - - IThrowableProxy cause = itp.getCause(); - if (cause != null) { - sb.append(VALUE_SEPARATOR); - appendThrowableProxy(sb, CAUSE_ATTR_NAME, cause); - } - - IThrowableProxy[] suppressedArray = itp.getSuppressed(); - if (suppressedArray != null && suppressedArray.length != 0) { - sb.append(VALUE_SEPARATOR); - sb.append(QUOTE).append(SUPPRESSED_ATTR_NAME).append(QUOTE_COL); - sb.append(OPEN_ARRAY); - boolean first = true; - for (IThrowableProxy suppressedITP : suppressedArray) { - if (first) { - first = false; - } else { - sb.append(VALUE_SEPARATOR); - } - appendThrowableProxy(sb, null, suppressedITP); - } - sb.append(CLOSE_ARRAY); - } - - sb.append(CLOSE_OBJ); - - } - - private void appendSTEPArray(StringBuilder sb, StackTraceElementProxy[] stepArray, int commonFrames) { - sb.append(QUOTE).append(STEP_ARRAY_NAME_ATTRIBUTE).append(QUOTE_COL).append(OPEN_ARRAY); - - int len = stepArray != null ? stepArray.length : 0; - - if (commonFrames >= len) { - commonFrames = 0; - } - - for (int i = 0; i < len - commonFrames; i++) { - if (i != 0) - sb.append(VALUE_SEPARATOR); - - StackTraceElementProxy step = stepArray[i]; - - sb.append(OPEN_OBJ); - StackTraceElement ste = step.getStackTraceElement(); - - appenderMember(sb, CLASS_NAME_ATTR_NAME, nullSafeStr(ste.getClassName())); - sb.append(VALUE_SEPARATOR); - - appenderMember(sb, METHOD_NAME_ATTR_NAME, nullSafeStr(ste.getMethodName())); - sb.append(VALUE_SEPARATOR); - - appenderMember(sb, FILE_NAME_ATTR_NAME, nullSafeStr(ste.getFileName())); - sb.append(VALUE_SEPARATOR); - - appenderMemberWithIntValue(sb, LINE_NUMBER_ATTR_NAME, ste.getLineNumber()); - sb.append(CLOSE_OBJ); - - } - - sb.append(CLOSE_ARRAY); - } - - private void appenderMember(StringBuilder sb, String key, String value) { + static void appenderMember(StringBuilder sb, String key, String value) { sb.append(QUOTE).append(key).append(QUOTE_COL).append(QUOTE).append(value).append(QUOTE); } - private void appenderMemberWithIntValue(StringBuilder sb, String key, int value) { + static void appenderMemberWithIntValue(StringBuilder sb, String key, int value) { sb.append(QUOTE).append(key).append(QUOTE_COL).append(value); } @@ -405,13 +306,13 @@ private String jsonEscapedToString(Object o) { return jsonEscapeString(o.toString()); } - private String nullSafeStr(String s) { + static String nullSafeStr(String s) { if (s == null) return NULL_STR; return s; } - private String jsonEscape(String s) { + static String jsonEscape(String s) { if (s == null) return NULL_STR; return jsonEscapeString(s); @@ -512,8 +413,15 @@ public void setWithThrowable(boolean withThrowable) { this.withThrowable = withThrowable; } + /** + * @param withPlainStackTrace + * @since 1.5.7 + */ + public void setWithPlainStackTrace(boolean withPlainStackTrace) { + this.withPlainStackTrace = withPlainStackTrace; + } + public void setWithFormattedMessage(boolean withFormattedMessage) { this.withFormattedMessage = withFormattedMessage; } - } diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/encoder/PlainStackTraceThrowableEncoder.java b/logback-classic/src/main/java/ch/qos/logback/classic/encoder/PlainStackTraceThrowableEncoder.java new file mode 100644 index 0000000000..cf7ad994aa --- /dev/null +++ b/logback-classic/src/main/java/ch/qos/logback/classic/encoder/PlainStackTraceThrowableEncoder.java @@ -0,0 +1,35 @@ +package ch.qos.logback.classic.encoder; + +import ch.qos.logback.classic.spi.IThrowableProxy; +import ch.qos.logback.classic.spi.StackTraceElementProxy; + +import static ch.qos.logback.classic.encoder.JsonEncoder.appenderMember; + +class PlainStackTraceThrowableEncoder extends ThrowableEncoder { + public static final String STACKTRACE_NAME_ATTRIBUTE = "stackTrace"; + + @Override + void encodeStackTrace(StringBuilder sb, IThrowableProxy itp) { + appenderMember(sb, STACKTRACE_NAME_ATTRIBUTE, getOriginalStackTrace(itp)); + } + + private static String getOriginalStackTrace(IThrowableProxy throwableProxy) { + StringBuilder sb = new StringBuilder(); + getOriginalStackTrace(throwableProxy, sb, 0); + return sb.toString(); + } + + private static void getOriginalStackTrace(IThrowableProxy throwable, StringBuilder sb, int depth) { + if (throwable == null) { + return; + } + if (depth > 0) { + sb.append("Caused by: "); + } + sb.append(throwable.getClassName()).append(": ").append(throwable.getMessage()).append(NEWLINE); + for (StackTraceElementProxy step : throwable.getStackTraceElementProxyArray()) { + sb.append(TAB).append(step.getSTEAsString()).append(NEWLINE); + } + getOriginalStackTrace(throwable.getCause(), sb, depth + 1); + } +} diff --git a/logback-classic/src/main/java/ch/qos/logback/classic/encoder/ThrowableEncoder.java b/logback-classic/src/main/java/ch/qos/logback/classic/encoder/ThrowableEncoder.java new file mode 100644 index 0000000000..2feac70821 --- /dev/null +++ b/logback-classic/src/main/java/ch/qos/logback/classic/encoder/ThrowableEncoder.java @@ -0,0 +1,66 @@ +package ch.qos.logback.classic.encoder; + +import ch.qos.logback.classic.spi.IThrowableProxy; + +import static ch.qos.logback.classic.encoder.JsonEncoder.CLASS_NAME_ATTR_NAME; +import static ch.qos.logback.classic.encoder.JsonEncoder.CLOSE_OBJ; +import static ch.qos.logback.classic.encoder.JsonEncoder.COMMON_FRAMES_COUNT_ATTR_NAME; +import static ch.qos.logback.classic.encoder.JsonEncoder.MESSAGE_ATTR_NAME; +import static ch.qos.logback.classic.encoder.JsonEncoder.OPEN_OBJ; +import static ch.qos.logback.classic.encoder.JsonEncoder.QUOTE; +import static ch.qos.logback.classic.encoder.JsonEncoder.QUOTE_COL; +import static ch.qos.logback.classic.encoder.JsonEncoder.THROWABLE_ATTR_NAME; +import static ch.qos.logback.classic.encoder.JsonEncoder.VALUE_SEPARATOR; +import static ch.qos.logback.classic.encoder.JsonEncoder.appenderMember; +import static ch.qos.logback.classic.encoder.JsonEncoder.appenderMemberWithIntValue; +import static ch.qos.logback.classic.encoder.JsonEncoder.jsonEscape; +import static ch.qos.logback.classic.encoder.JsonEncoder.nullSafeStr; +import static ch.qos.logback.core.model.ModelConstants.NULL_STR; + +abstract class ThrowableEncoder { + + static final String NEWLINE = "\\n"; + static final String TAB = "\\t"; + + private static final String CYCLIC_THROWABLE_ATTR_NAME = "cyclic"; + + static void appendThrowableProxy(StringBuilder sb, IThrowableProxy throwableProxy, boolean withPlainStackTrace) { + ThrowableEncoder throwableEncoder = withPlainStackTrace ? new PlainStackTraceThrowableEncoder() : new ArrayThrowableEncoder(); + throwableEncoder.appendThrowableProxy(sb, THROWABLE_ATTR_NAME, throwableProxy); + } + + protected void appendThrowableProxy(StringBuilder sb, String attributeName, IThrowableProxy itp) { + // in the nominal case, attributeName != null. However, attributeName will be null for suppressed + // IThrowableProxy array, in which case no attribute name is needed + if (attributeName != null) { + sb.append(QUOTE).append(attributeName).append(QUOTE_COL); + if (itp == null) { + sb.append(NULL_STR); + return; + } + } + + sb.append(OPEN_OBJ); + + appenderMember(sb, CLASS_NAME_ATTR_NAME, nullSafeStr(itp.getClassName())); + + sb.append(VALUE_SEPARATOR); + appenderMember(sb, MESSAGE_ATTR_NAME, jsonEscape(itp.getMessage())); + + if (itp.isCyclic()) { + sb.append(VALUE_SEPARATOR); + appenderMember(sb, CYCLIC_THROWABLE_ATTR_NAME, jsonEscape("true")); + } + + if (itp.getCommonFrames() != 0) { + sb.append(VALUE_SEPARATOR); + appenderMemberWithIntValue(sb, COMMON_FRAMES_COUNT_ATTR_NAME, itp.getCommonFrames()); + } + + sb.append(VALUE_SEPARATOR); + encodeStackTrace(sb, itp); + sb.append(CLOSE_OBJ); + } + + abstract void encodeStackTrace(StringBuilder sb, IThrowableProxy itp); +} diff --git a/logback-classic/src/test/java/ch/qos/logback/classic/encoder/JsonEncoderTest.java b/logback-classic/src/test/java/ch/qos/logback/classic/encoder/JsonEncoderTest.java index c71672f168..c9fe07043f 100644 --- a/logback-classic/src/test/java/ch/qos/logback/classic/encoder/JsonEncoderTest.java +++ b/logback-classic/src/test/java/ch/qos/logback/classic/encoder/JsonEncoderTest.java @@ -29,7 +29,6 @@ import ch.qos.logback.core.read.ListAppender; import ch.qos.logback.core.status.testUtil.StatusChecker; import ch.qos.logback.core.testUtil.RandomUtil; -import ch.qos.logback.core.util.StatusPrinter; import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -48,6 +47,7 @@ import java.util.Map; import java.util.Objects; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -257,6 +257,22 @@ void withThrowable() throws JsonProcessingException { compareEvents(event, resultEvent); } + @Test + void withPlainStackTrace() throws IOException { + Throwable t = new RuntimeException("test", new IllegalStateException("test cause")); + LoggingEvent event = new LoggingEvent("in withThrowable test", logger, Level.WARN, "hello kvp", t, null); + + jsonEncoder.setWithPlainStackTrace(true); + byte[] resultBytes = jsonEncoder.encode(event); + String resultString = new String(resultBytes, StandardCharsets.UTF_8).trim(); + + //testing the whole stack trace is brittle - depends on the tool used, e.g. is different in an IDE + //and contains line numbers that can easily change + + assertThat(resultString).contains("\"java.lang.RuntimeException: test\\n\\tat ch.qos.logback.classic"); + assertThat(resultString).contains("Caused by: java.lang.IllegalStateException: test cause\\n\\tat ch.qos.logback.classic"); + } + @Test void withThrowableHavingCause() throws JsonProcessingException { Throwable cause = new IllegalStateException("test cause"); @@ -371,4 +387,4 @@ void withJoranAndEnabledFormattedMessage() throws JoranException, IOException { assertEquals(withness, lines.get(0)); } -} \ No newline at end of file +}