diff --git a/README.md b/README.md index 8dda6f8..692cc37 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,36 @@ Default max stack trace size is `300`. It is recommended that you do not exceed lead to higher log event ingest costs. Max stack trace size can be configured by environment variable (`NEW_RELIC_LOG_EXTENSION_MAX_STACK_SIZE=integer`) or system property (`-Dnewrelic.log_extension.max_stack_size=integer`). +### Include full exception stack trace +Only supported on logback + +You can configure the logging extension to include in `error.stack` the full stack trace just like the downstream library does. +This includes the caused by chain. + +As an example, this snippet: +``` +Exception inner2 = new IllegalAccessException("test caused by 2"); +Exception inner1 = new RuntimeException("test caused by 1", inner2); +LOGGER.error("failed for test!", new RuntimeException("test exception", inner1)); +``` +Will set the `error.stack` as +``` +java.lang.RuntimeException: test exception + at (regular stacktrace) + at ... +Caused by: java.lang.RuntimeException: test caused by 1 + ... 49 common frames omitted +Caused by: java.lang.IllegalAccessException: test caused by 2 + ... 49 common frames omitted +``` + +Default is `false` for retro-compatibility. + +This setting can be enabled via environment variable (`NEW_RELIC_LOG_EXTENSION_INCLUDE_FULL_ERROR_STACK=true`) or system variable (`-Dnewrelic.log_extension.include_full_error_stack=true`). + +Be aware that enabling this setting will use the downstream library's stacktrace generation, not taking in consideration +the value of [Exception Stack Trace Size](#exception-stack-trace-size). + ## Support Should you need assistance with New Relic products, you are in good hands with several diagnostic tools and support channels. diff --git a/core/src/main/java/com/newrelic/logging/core/LogExtensionConfig.java b/core/src/main/java/com/newrelic/logging/core/LogExtensionConfig.java index 2c60f93..3e61816 100644 --- a/core/src/main/java/com/newrelic/logging/core/LogExtensionConfig.java +++ b/core/src/main/java/com/newrelic/logging/core/LogExtensionConfig.java @@ -13,6 +13,9 @@ public class LogExtensionConfig { public static final String MAX_STACK_SIZE_SYS_PROP = CONFIG_PREFIX_SYS_PROP + "max_stack_size"; public static final String ADD_MDC_ENV_VAR = CONFIG_PREFIX_ENV_VAR + "ADD_MDC"; public static final String ADD_MDC_SYS_PROP = CONFIG_PREFIX_SYS_PROP + "add_mdc"; + + public static final String INCLUDE_FULL_ERROR_STACK_ENV = CONFIG_PREFIX_ENV_VAR + "INCLUDE_FULL_ERROR_STACK"; + public static final String INCLUDE_FULL_ERROR_SYS_PROP = CONFIG_PREFIX_SYS_PROP + "include_full_error_stack"; public static final boolean ADD_MDC_DEFAULT = false; /** @@ -35,6 +38,24 @@ public static int getMaxStackSize() { } } + /** + * Get a boolean representing if error stack should include the full stacktrace, including the "caused by" chain + *

Default, for retro-compatibility, is {@code false}. + * @return boolean representing include full error stack configuration + */ + public static boolean getIncludeFullErrorStacktrace(){ + String envVar = System.getenv(INCLUDE_FULL_ERROR_STACK_ENV); + String sysProp = System.getProperty(INCLUDE_FULL_ERROR_SYS_PROP); + + if (envVar != null){ + return Boolean.parseBoolean(envVar); + } else if (sysProp != null) { + return Boolean.parseBoolean(sysProp); + } else { + return false; + } + } + /** * Get a boolean indicating if MDC should be added to logs *

diff --git a/logback/README.md b/logback/README.md index 5c7b8cd..6d4f1ee 100644 --- a/logback/README.md +++ b/logback/README.md @@ -11,7 +11,8 @@ There are some changes to your application to use the New Relic Logback Extension. All steps are required. -**Optional**: [Configuration Options](..%2FREADME.md#configuration-options) for setting max stack size or collecting MDC. +**Optional**: [Configuration Options](..%2FREADME.md#configuration-options) for setting max stack size (or to include +the full stack trace in `error.stack`) or collecting MDC. ### 1. Include the dependency in your project. diff --git a/logback/src/main/java/com/newrelic/logging/logback/NewRelicEncoder.java b/logback/src/main/java/com/newrelic/logging/logback/NewRelicEncoder.java index b2422b9..d293588 100644 --- a/logback/src/main/java/com/newrelic/logging/logback/NewRelicEncoder.java +++ b/logback/src/main/java/com/newrelic/logging/logback/NewRelicEncoder.java @@ -12,6 +12,7 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; +import static com.newrelic.logging.core.LogExtensionConfig.getIncludeFullErrorStacktrace; import static com.newrelic.logging.core.LogExtensionConfig.getMaxStackSize; /** @@ -49,7 +50,7 @@ public byte[] encode(ILoggingEvent event) { @Override public void start() { super.start(); - layout = new NewRelicJsonLayout(maxStackSize); + layout = new NewRelicJsonLayout(maxStackSize, getIncludeFullErrorStacktrace()); layout.start(); } diff --git a/logback/src/main/java/com/newrelic/logging/logback/NewRelicJsonLayout.java b/logback/src/main/java/com/newrelic/logging/logback/NewRelicJsonLayout.java index 40ae3cf..286c9b6 100644 --- a/logback/src/main/java/com/newrelic/logging/logback/NewRelicJsonLayout.java +++ b/logback/src/main/java/com/newrelic/logging/logback/NewRelicJsonLayout.java @@ -8,6 +8,7 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.IThrowableProxy; import ch.qos.logback.classic.spi.StackTraceElementProxy; +import ch.qos.logback.classic.spi.ThrowableProxyUtil; import ch.qos.logback.core.LayoutBase; import com.fasterxml.jackson.core.JsonGenerator; import com.newrelic.logging.core.ElementName; @@ -21,18 +22,21 @@ import java.util.Map; import static com.newrelic.logging.core.LogExtensionConfig.CONTEXT_PREFIX; +import static com.newrelic.logging.core.LogExtensionConfig.getIncludeFullErrorStacktrace; import static com.newrelic.logging.core.LogExtensionConfig.getMaxStackSize; import static com.newrelic.logging.logback.NewRelicAsyncAppender.NEW_RELIC_PREFIX; public class NewRelicJsonLayout extends LayoutBase { private final Integer maxStackSize; + private final boolean includeFullErrorStacktrace; public NewRelicJsonLayout() { - this(getMaxStackSize()); + this(getMaxStackSize(), getIncludeFullErrorStacktrace()); } - public NewRelicJsonLayout(Integer maxStackSize) { + public NewRelicJsonLayout(Integer maxStackSize, boolean includeFullErrorStacktrace) { this.maxStackSize = maxStackSize; + this.includeFullErrorStacktrace = includeFullErrorStacktrace; } @Override @@ -96,14 +100,18 @@ private void writeToGenerator(ILoggingEvent event, JsonGenerator generator) thro generator.writeObjectField(ElementName.ERROR_CLASS, proxy.getClassName()); generator.writeObjectField(ElementName.ERROR_MESSAGE, proxy.getMessage()); - StackTraceElementProxy[] stackProxy = proxy.getStackTraceElementProxyArray(); - if (stackProxy != null && stackProxy.length > 0) { - List elements = new ArrayList<>(maxStackSize); - for (int i = 0; i < maxStackSize && i < stackProxy.length; i++) { - elements.add(stackProxy[i].getStackTraceElement()); + if (includeFullErrorStacktrace){ + generator.writeObjectField(ElementName.ERROR_STACK, ThrowableProxyUtil.asString(proxy)); + } else { + StackTraceElementProxy[] stackProxy = proxy.getStackTraceElementProxyArray(); + if (stackProxy != null && stackProxy.length > 0) { + List elements = new ArrayList<>(maxStackSize); + for (int i = 0; i < maxStackSize && i < stackProxy.length; i++) { + elements.add(stackProxy[i].getStackTraceElement()); + } + + generator.writeObjectField(ElementName.ERROR_STACK, ExceptionUtil.getErrorStack(elements.toArray(new StackTraceElement[0]), maxStackSize)); } - - generator.writeObjectField(ElementName.ERROR_STACK, ExceptionUtil.getErrorStack(elements.toArray(new StackTraceElement[0]), maxStackSize)); } } diff --git a/logback/src/test/java/com/newrelic/logging/logback/NewRelicLogbackTests.java b/logback/src/test/java/com/newrelic/logging/logback/NewRelicLogbackTests.java index 6402df2..12a5d42 100644 --- a/logback/src/test/java/com/newrelic/logging/logback/NewRelicLogbackTests.java +++ b/logback/src/test/java/com/newrelic/logging/logback/NewRelicLogbackTests.java @@ -89,6 +89,17 @@ void shouldAppendErrorDataCorrectly() throws Throwable { thenTheExceptionDataIsInTheMessage(); } + @Test + @Timeout(3) + void shouldAppendFullErrorDataCorrectly() throws Throwable { + givenMockAgentData(); + givenARedirectedAppender(); + givenALoggingEventWithExceptionDataIncludingCausedBy(); + whenTheEventIsAppended(); + thenJsonLayoutWasUsed(); + thenTheExceptionCausedByDataIsInTheMessage(); + } + @Test @Timeout(3) void shouldAppendCustomArgsToJsonCorrectly() throws Throwable { @@ -143,6 +154,12 @@ private void givenALoggingEventWithExceptionData() { event.setThrowableProxy(new ThrowableProxy(new Exception("~~ oops ~~"))); } + private void givenALoggingEventWithExceptionDataIncludingCausedBy() { + System.setProperty("newrelic.log_extension.include_full_error_stack", "true"); + givenALoggingEvent(); + event.setThrowableProxy(new ThrowableProxy(new Exception("~~ oops ~~", new Exception("~~ oops inner 1 ~~", new Exception("~~ oops inner 2 ~~"))))); + } + private void givenALoggingEventWithCallerData() { givenALoggingEvent(); event.setCallerData(new StackTraceElement[] { new Exception().getStackTrace()[0] }); @@ -240,6 +257,16 @@ private void thenTheExceptionDataIsInTheMessage() throws Throwable { ); } + private void thenTheExceptionCausedByDataIsInTheMessage() throws Throwable { + LogAsserts.assertFieldValues( + getOutput(), + ImmutableMap.of( + "error.stack.causedby", Pattern.compile(".*Caused By.*", Pattern.DOTALL), + "error.stack.inner1", Pattern.compile(".*oops inner 1.*", Pattern.DOTALL), + "error.stack.inner2", Pattern.compile(".*oops inner 2.*", Pattern.DOTALL) + )); + } + private void thenTheCustomArgsAreInTheMessage() throws Throwable { LogAsserts.assertFieldValues( getOutput(),