diff --git a/README.md b/README.md index d974dc2d..09a745b8 100644 --- a/README.md +++ b/README.md @@ -1634,6 +1634,19 @@ For example: See the [net.logstash.logback.decorate](/src/main/java/net/logstash/logback/decorate) package and sub-packages for other decorators. +If you prefer pretty printing for easier interactive viewing of (error) logs, you +may also prefer to write the stacktrace as an array of strings where each string +is a stacktrace line: + +```xml + + + true + + true + +``` + ### Registering Jackson Modules By default, Jackson modules are dynamically registered via @@ -2871,6 +2884,7 @@ The provider name is the xml element name to use when configuring. Each provider diff --git a/src/main/java/net/logstash/logback/LogstashFormatter.java b/src/main/java/net/logstash/logback/LogstashFormatter.java index 75f6288e..0ec312ef 100644 --- a/src/main/java/net/logstash/logback/LogstashFormatter.java +++ b/src/main/java/net/logstash/logback/LogstashFormatter.java @@ -396,6 +396,14 @@ public void setThrowableConverter(ThrowableHandlingConverter throwableConverter) this.stackTraceProvider.setThrowableConverter(throwableConverter); } + public boolean isWriteStackTraceAsArray() { + return this.stackTraceProvider.isWriteAsArray(); + } + + public void setWriteStackTraceAsArray(boolean writeStackTraceAsArray) { + this.stackTraceProvider.setWriteAsArray(writeStackTraceAsArray); + } + public String getVersion() { return this.versionProvider.getVersion(); } diff --git a/src/main/java/net/logstash/logback/composite/loggingevent/StackTraceJsonProvider.java b/src/main/java/net/logstash/logback/composite/loggingevent/StackTraceJsonProvider.java index 52f88b02..a4987502 100644 --- a/src/main/java/net/logstash/logback/composite/loggingevent/StackTraceJsonProvider.java +++ b/src/main/java/net/logstash/logback/composite/loggingevent/StackTraceJsonProvider.java @@ -41,6 +41,12 @@ public class StackTraceJsonProvider extends AbstractFieldJsonProvider> implements MapperBuilderDecorator { + private static final DefaultPrettyPrinter.FixedSpaceIndenter DEFAULT_ARRAY_INDENTER = DefaultPrettyPrinter.FixedSpaceIndenter.instance(); + private Separators separators = PrettyPrinter.DEFAULT_SEPARATORS .withRootSeparator(CoreConstants.EMPTY_STRING); - private DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(separators); + private DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(separators) + .withArrayIndenter(DEFAULT_ARRAY_INDENTER); @Override public B decorate(B mapperBuilder) { @@ -66,4 +70,17 @@ public void setSpacesInObjectEntries(boolean spacesInObjectEntries) { separators = separators.withObjectNameValueSpacing(spacesInObjectEntries ? Separators.Spacing.BOTH : Separators.Spacing.NONE); prettyPrinter = prettyPrinter.withSeparators(separators); } + + /** + * Sets whether arrays are indented with a new line. + * + * @param indentArraysWithNewLine whether arrays are indented with a new line. + */ + public void setIndentArraysWithNewLine(boolean indentArraysWithNewLine) { + if (indentArraysWithNewLine) { + prettyPrinter = prettyPrinter.withArrayIndenter(DefaultIndenter.SYSTEM_LINEFEED_INSTANCE); + } else { + prettyPrinter = prettyPrinter.withArrayIndenter(DEFAULT_ARRAY_INDENTER); + } + } } diff --git a/src/main/java/net/logstash/logback/encoder/LogstashEncoder.java b/src/main/java/net/logstash/logback/encoder/LogstashEncoder.java index 94cb4aae..58805fdd 100644 --- a/src/main/java/net/logstash/logback/encoder/LogstashEncoder.java +++ b/src/main/java/net/logstash/logback/encoder/LogstashEncoder.java @@ -203,6 +203,14 @@ public void setThrowableConverter(ThrowableHandlingConverter throwableConverter) getFormatter().setThrowableConverter(throwableConverter); } + public boolean isWriteStackTraceAsArray() { + return getFormatter().isWriteStackTraceAsArray(); + } + + public void setWriteStackTraceAsArray(boolean writeStackTraceAsArray) { + this.getFormatter().setWriteStackTraceAsArray(writeStackTraceAsArray); + } + public String getTimeZone() { return getFormatter().getTimeZone(); } diff --git a/src/main/java/net/logstash/logback/layout/LogstashLayout.java b/src/main/java/net/logstash/logback/layout/LogstashLayout.java index 0d473958..b0621320 100644 --- a/src/main/java/net/logstash/logback/layout/LogstashLayout.java +++ b/src/main/java/net/logstash/logback/layout/LogstashLayout.java @@ -215,7 +215,15 @@ public void setTimestampPattern(String pattern) { public void setThrowableConverter(ThrowableHandlingConverter throwableConverter) { getFormatter().setThrowableConverter(throwableConverter); } - + + public boolean isWriteStackTraceAsArray() { + return getFormatter().isWriteStackTraceAsArray(); + } + + public void setWriteStackTraceAsArray(boolean writeStackTraceAsArray) { + this.getFormatter().setWriteStackTraceAsArray(writeStackTraceAsArray); + } + public String getVersion() { return getFormatter().getVersion(); } diff --git a/src/test/java/net/logstash/logback/composite/loggingevent/StackTraceJsonProviderTest.java b/src/test/java/net/logstash/logback/composite/loggingevent/StackTraceJsonProviderTest.java index 3814eead..a2e5bc0e 100644 --- a/src/test/java/net/logstash/logback/composite/loggingevent/StackTraceJsonProviderTest.java +++ b/src/test/java/net/logstash/logback/composite/loggingevent/StackTraceJsonProviderTest.java @@ -15,6 +15,8 @@ */ package net.logstash.logback.composite.loggingevent; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -25,6 +27,7 @@ import ch.qos.logback.classic.pattern.ThrowableHandlingConverter; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.IThrowableProxy; +import org.assertj.core.util.Throwables; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -90,4 +93,22 @@ public void testFieldNames() { verify(generator).writeStringProperty("newFieldName", "stack"); } + @Test + public void testWriteAsArray() { + String stacktrace = Throwables.getStackTrace(new RuntimeException("testing exception handling")); + when(converter.convert(event)).thenReturn(stacktrace); + + provider.setWriteAsArray(true); + + when(event.getThrowableProxy()).thenReturn(ThrowableProxy); + + provider.writeTo(generator, event); + + verify(generator).writeName("stack_trace"); + verify(generator).writeStartArray(); + verify(generator).writeString("java.lang.RuntimeException: testing exception handling"); + verify(generator, atLeastOnce()).writeString(anyString()); + verify(generator).writeEndArray(); + } + } diff --git a/src/test/java/net/logstash/logback/decorate/PrettyPrintingDecoratorTest.java b/src/test/java/net/logstash/logback/decorate/PrettyPrintingDecoratorTest.java index d88135c4..954378b6 100644 --- a/src/test/java/net/logstash/logback/decorate/PrettyPrintingDecoratorTest.java +++ b/src/test/java/net/logstash/logback/decorate/PrettyPrintingDecoratorTest.java @@ -19,6 +19,7 @@ import java.io.StringWriter; import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.Test; import tools.jackson.core.JsonGenerator; @@ -89,4 +90,29 @@ void noSpacesInObjectEntries() { writer.flush(); assertThat(writer.toString()).isEqualTo("{\n \"key1\":\"value1\"\n}{\n \"key2\":\"value2\"\n}"); } + + @Test + void arrayElementsOnNewLine() { + PrettyPrintingDecorator decorator = new PrettyPrintingDecorator<>(); + decorator.setIndentArraysWithNewLine(true); + + StringWriter writer = new StringWriter(); + JsonGenerator generator = decorator.decorate(JsonMapper.builder()).build().createGenerator(writer); + + generator.writePOJO(Collections.singletonMap("key1", List.of( + "RuntimeException: foobar", + "\tat com.example.Foobar"))); + generator.writePOJO(Collections.singletonMap("key2", "value2")); + + generator.flush(); + writer.flush(); + assertThat(writer.toString()).isEqualTo("{\n" + + " \"key1\" : [\n" + + " \"RuntimeException: foobar\",\n" + + " \"\\tat com.example.Foobar\"\n" + + " ]\n" + + "}{\n" + + " \"key2\" : \"value2\"\n" + + "}"); + } }