diff --git a/README.md b/README.md
index 36871477..e250b732 100644
--- a/README.md
+++ b/README.md
@@ -2160,15 +2160,31 @@ The provider name is the xml element name to use when configuring. Each provider
+
+ throwableMessage |
+ (Only if a throwable was logged) Outputs a field that contains the message of the thrown Throwable.
+
+ - fieldName - Output field name (throwable_message)
+
+ |
+
throwableRootCauseClassName |
- (Only if a throwable was logged) Outputs a field that contains the class name of the root cause of the thrown Throwable.
+ | (Only if a throwable was logged and a root cause could be determined) Outputs a field that contains the class name of the root cause of the thrown Throwable.
- fieldName - Output field name (throwable_root_cause_class)
- useSimpleClassName - When true, the throwable's simple class name will be used. When false, the fully qualified class name will be used. (true)
|
-
+
+
+ throwableRootCauseMessage |
+ (Only if a throwable was logged and a root cause could be determined) Outputs a field that contains the message of the root cause of the thrown Throwable.
+
+ - fieldName - Output field name (throwable_root_cause_message)
+
+ |
+
diff --git a/src/main/java/net/logstash/logback/composite/loggingevent/AbstractThrowableMessageJsonProvider.java b/src/main/java/net/logstash/logback/composite/loggingevent/AbstractThrowableMessageJsonProvider.java
new file mode 100644
index 00000000..bfc23cae
--- /dev/null
+++ b/src/main/java/net/logstash/logback/composite/loggingevent/AbstractThrowableMessageJsonProvider.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2013-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.logstash.logback.composite.loggingevent;
+
+import java.io.IOException;
+
+import net.logstash.logback.composite.AbstractFieldJsonProvider;
+import net.logstash.logback.composite.JsonWritingUtils;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.IThrowableProxy;
+import com.fasterxml.jackson.core.JsonGenerator;
+
+/**
+ * Logs an exception message for a given logging event. Which exception to be
+ * logged depends on the subclass's implementation of
+ * {@link #getThrowable(ILoggingEvent)}.
+ */
+public abstract class AbstractThrowableMessageJsonProvider extends AbstractFieldJsonProvider {
+
+ protected AbstractThrowableMessageJsonProvider(String fieldName) {
+ setFieldName(fieldName);
+ }
+
+ @Override
+ public void writeTo(JsonGenerator generator, ILoggingEvent event) throws IOException {
+ IThrowableProxy throwable = getThrowable(event);
+ if (throwable != null) {
+ String throwableMessage = throwable.getMessage();
+ JsonWritingUtils.writeStringField(generator, getFieldName(), throwableMessage);
+ }
+ }
+
+ /**
+ * @param event the event being logged, never {@code null}
+ * @return the throwable to use, or {@code null} if no appropriate throwable is
+ * available
+ * @throws NullPointerException if {@code event} is {@code null}
+ */
+ protected abstract IThrowableProxy getThrowable(ILoggingEvent event);
+}
diff --git a/src/main/java/net/logstash/logback/composite/loggingevent/LoggingEventJsonProviders.java b/src/main/java/net/logstash/logback/composite/loggingevent/LoggingEventJsonProviders.java
index 041a4c54..62062f59 100644
--- a/src/main/java/net/logstash/logback/composite/loggingevent/LoggingEventJsonProviders.java
+++ b/src/main/java/net/logstash/logback/composite/loggingevent/LoggingEventJsonProviders.java
@@ -91,7 +91,13 @@ public void addNestedField(LoggingEventNestedJsonProvider provider) {
public void addThrowableClassName(ThrowableClassNameJsonProvider provider) {
addProvider(provider);
}
+ public void addThrowableMessage(ThrowableMessageJsonProvider provider) {
+ addProvider(provider);
+ }
public void addThrowableRootCauseClassName(ThrowableRootCauseClassNameJsonProvider provider) {
addProvider(provider);
}
+ public void addThrowableRootCauseMessage(ThrowableRootCauseMessageJsonProvider provider) {
+ addProvider(provider);
+ }
}
diff --git a/src/main/java/net/logstash/logback/composite/loggingevent/ThrowableMessageJsonProvider.java b/src/main/java/net/logstash/logback/composite/loggingevent/ThrowableMessageJsonProvider.java
new file mode 100644
index 00000000..9640669e
--- /dev/null
+++ b/src/main/java/net/logstash/logback/composite/loggingevent/ThrowableMessageJsonProvider.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2013-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.logstash.logback.composite.loggingevent;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.IThrowableProxy;
+
+/**
+ * Logs the message of the throwable associated with a given logging event, if
+ * any.
+ */
+public class ThrowableMessageJsonProvider extends AbstractThrowableMessageJsonProvider {
+
+ public ThrowableMessageJsonProvider() {
+ super("throwable_message");
+ }
+
+ @Override
+ protected IThrowableProxy getThrowable(ILoggingEvent event) {
+ return event.getThrowableProxy();
+ }
+}
diff --git a/src/main/java/net/logstash/logback/composite/loggingevent/ThrowableRootCauseClassNameJsonProvider.java b/src/main/java/net/logstash/logback/composite/loggingevent/ThrowableRootCauseClassNameJsonProvider.java
index afa1a853..255d5320 100644
--- a/src/main/java/net/logstash/logback/composite/loggingevent/ThrowableRootCauseClassNameJsonProvider.java
+++ b/src/main/java/net/logstash/logback/composite/loggingevent/ThrowableRootCauseClassNameJsonProvider.java
@@ -17,6 +17,11 @@
import ch.qos.logback.classic.spi.IThrowableProxy;
+/**
+ * Logs the class name of the innermost cause of the throwable associated with a
+ * given logging event, if any. The root cause may be the throwable itself, if
+ * it has no cause.
+ */
public class ThrowableRootCauseClassNameJsonProvider extends AbstractThrowableClassNameJsonProvider {
static final String FIELD_NAME = "throwable_root_cause_class";
@@ -26,13 +31,6 @@ public ThrowableRootCauseClassNameJsonProvider() {
@Override
IThrowableProxy getThrowable(IThrowableProxy throwable) {
- return getCause(throwable);
- }
-
- /**
- * @return given throwable if t does not contain any cause; null if given throwable is null
- */
- private static IThrowableProxy getCause(IThrowableProxy t) {
- return (t != null && t.getCause() != null) ? getCause(t.getCause()) : t;
+ return throwable == null ? null : ThrowableSelectors.rootCause(throwable);
}
}
diff --git a/src/main/java/net/logstash/logback/composite/loggingevent/ThrowableRootCauseMessageJsonProvider.java b/src/main/java/net/logstash/logback/composite/loggingevent/ThrowableRootCauseMessageJsonProvider.java
new file mode 100644
index 00000000..f6af10f9
--- /dev/null
+++ b/src/main/java/net/logstash/logback/composite/loggingevent/ThrowableRootCauseMessageJsonProvider.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2013-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.logstash.logback.composite.loggingevent;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.IThrowableProxy;
+
+/**
+ * Logs the message of the innermost cause of the throwable associated with a
+ * given logging event, if any. The root cause may be the throwable itself, if
+ * it has no cause.
+ */
+public class ThrowableRootCauseMessageJsonProvider extends AbstractThrowableMessageJsonProvider {
+
+ public ThrowableRootCauseMessageJsonProvider() {
+ super("throwable_root_cause_message");
+ }
+
+ @Override
+ protected IThrowableProxy getThrowable(ILoggingEvent event) {
+ IThrowableProxy t = event.getThrowableProxy();
+ return t == null ? null : ThrowableSelectors.rootCause(t);
+ }
+}
diff --git a/src/main/java/net/logstash/logback/composite/loggingevent/ThrowableSelectors.java b/src/main/java/net/logstash/logback/composite/loggingevent/ThrowableSelectors.java
new file mode 100644
index 00000000..0160d928
--- /dev/null
+++ b/src/main/java/net/logstash/logback/composite/loggingevent/ThrowableSelectors.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2013-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.logstash.logback.composite.loggingevent;
+
+import ch.qos.logback.classic.spi.IThrowableProxy;
+
+/**
+ * Utilities to obtain {@code Throwables} from {@code IThrowableProxies}.
+ */
+public class ThrowableSelectors {
+
+ /**
+ * Returns the innermost cause of {@code throwable}.
+ *
+ * @return the innermost cause, which may be {@code throwable} itself if there
+ * is no cause, or {@code null} if there is a loop in the causal chain.
+ *
+ * @throws NullPointerException if {@code throwable} is {@code null}
+ */
+ public static IThrowableProxy rootCause(IThrowableProxy throwable) {
+ // Keep a second pointer that slowly walks the causal chain.
+ // If the fast pointer ever catches the slower pointer, then there's a loop.
+ IThrowableProxy slowPointer = throwable;
+ boolean advanceSlowPointer = false;
+
+ IThrowableProxy cause;
+ while ((cause = throwable.getCause()) != null) {
+ throwable = cause;
+
+ if (throwable == slowPointer) {
+ // There's a cyclic reference, so no real root cause.
+ return null;
+ }
+
+ if (advanceSlowPointer) {
+ slowPointer = slowPointer.getCause();
+ }
+
+ advanceSlowPointer = !advanceSlowPointer; // only advance every other iteration
+ }
+
+ return throwable;
+ }
+
+}
diff --git a/src/test/java/net/logstash/logback/composite/loggingevent/ThrowableMessageJsonProviderTest.java b/src/test/java/net/logstash/logback/composite/loggingevent/ThrowableMessageJsonProviderTest.java
new file mode 100644
index 00000000..a0f8cfaa
--- /dev/null
+++ b/src/test/java/net/logstash/logback/composite/loggingevent/ThrowableMessageJsonProviderTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2013-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.logstash.logback.composite.loggingevent;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.ThrowableProxy;
+import com.fasterxml.jackson.core.JsonGenerator;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class ThrowableMessageJsonProviderTest {
+
+ private ThrowableMessageJsonProvider provider = new ThrowableMessageJsonProvider();
+
+ @Mock
+ private JsonGenerator generator;
+
+ @Mock
+ private ILoggingEvent event;
+
+ @Test
+ public void testFieldName() throws IOException {
+ provider.setFieldName("newFieldName");
+
+ IOException throwable = new IOException("kaput");
+ when(event.getThrowableProxy()).thenReturn(new ThrowableProxy(throwable));
+
+ provider.writeTo(generator, event);
+
+ verify(generator).writeStringField("newFieldName", "kaput");
+ }
+
+ @Test
+ public void testNoThrowable() throws IOException {
+ when(event.getThrowableProxy()).thenReturn(null);
+
+ provider.writeTo(generator, event);
+
+ verify(event, atLeastOnce()).getThrowableProxy();
+ verifyNoInteractions(generator);
+ }
+}
diff --git a/src/test/java/net/logstash/logback/composite/loggingevent/ThrowableRootCauseClassNameJsonProviderTest.java b/src/test/java/net/logstash/logback/composite/loggingevent/ThrowableRootCauseClassNameJsonProviderTest.java
index 8543747e..7b6c55aa 100644
--- a/src/test/java/net/logstash/logback/composite/loggingevent/ThrowableRootCauseClassNameJsonProviderTest.java
+++ b/src/test/java/net/logstash/logback/composite/loggingevent/ThrowableRootCauseClassNameJsonProviderTest.java
@@ -16,13 +16,17 @@
package net.logstash.logback.composite.loggingevent;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import java.io.IOException;
import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.ThrowableProxy;
import com.fasterxml.jackson.core.JsonGenerator;
import org.junit.jupiter.api.Test;
@@ -78,4 +82,21 @@ public void testNoThrowable() throws IOException {
verify(generator, times(0)).writeStringField(anyString(), anyString());
}
+
+ @Test
+ public void testCircularReference() throws IOException {
+ IThrowableProxy baz = mock(IThrowableProxy.class, "baz");
+ IThrowableProxy bar = mock(IThrowableProxy.class, "bar");
+ IThrowableProxy foo = mock(IThrowableProxy.class, "foo");
+ when(foo.getCause()).thenReturn(bar);
+ when(bar.getCause()).thenReturn(baz);
+ when(baz.getCause()).thenReturn(foo);
+
+ when(event.getThrowableProxy()).thenReturn(foo);
+
+ provider.writeTo(generator, event);
+
+ verify(event, atLeastOnce()).getThrowableProxy();
+ verifyNoInteractions(generator);
+ }
}
diff --git a/src/test/java/net/logstash/logback/composite/loggingevent/ThrowableRootCauseMessageJsonProviderTest.java b/src/test/java/net/logstash/logback/composite/loggingevent/ThrowableRootCauseMessageJsonProviderTest.java
new file mode 100644
index 00000000..2644da9f
--- /dev/null
+++ b/src/test/java/net/logstash/logback/composite/loggingevent/ThrowableRootCauseMessageJsonProviderTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2013-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.logstash.logback.composite.loggingevent;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.IThrowableProxy;
+import ch.qos.logback.classic.spi.ThrowableProxy;
+import com.fasterxml.jackson.core.JsonGenerator;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class ThrowableRootCauseMessageJsonProviderTest {
+
+ private AbstractThrowableMessageJsonProvider provider = new ThrowableRootCauseMessageJsonProvider();
+
+ @Mock
+ private JsonGenerator generator;
+
+ @Mock
+ private ILoggingEvent event;
+
+ @Test
+ public void testNoThrowable() throws IOException {
+ when(event.getThrowableProxy()).thenReturn(null);
+
+ provider.writeTo(generator, event);
+
+ verify(event, atLeastOnce()).getThrowableProxy();
+ verifyNoInteractions(generator);
+ }
+
+ @Test
+ public void testDefaultFieldName() throws IOException {
+ when(event.getThrowableProxy()).thenReturn(new ThrowableProxy(new Exception("kaput")));
+
+ provider.writeTo(generator, event);
+
+ verify(generator).writeStringField("throwable_root_cause_message", "kaput");
+ }
+
+ @Test
+ public void testCustomFieldName() throws IOException {
+ when(event.getThrowableProxy()).thenReturn(new ThrowableProxy(new Exception("kaput")));
+
+ provider.setFieldName("some_custom_field");
+ provider.writeTo(generator, event);
+
+ verify(generator).writeStringField("some_custom_field", "kaput");
+ }
+
+ @Test
+ public void testNestedException() throws IOException {
+ Exception foo = new Exception("foo", new Exception("bar", new Exception("baz")));
+ when(event.getThrowableProxy()).thenReturn(new ThrowableProxy(foo));
+
+ provider.writeTo(generator, event);
+
+ verify(generator).writeStringField(anyString(), eq("baz"));
+ }
+
+ @Test
+ public void testCircularReference() throws IOException {
+ IThrowableProxy foo = mock(IThrowableProxy.class, "foo");
+ IThrowableProxy bar = mock(IThrowableProxy.class, "bar");
+ IThrowableProxy baz = mock(IThrowableProxy.class, "baz");
+ when(foo.getCause()).thenReturn(bar);
+ when(bar.getCause()).thenReturn(baz);
+ when(baz.getCause()).thenReturn(foo);
+
+ when(event.getThrowableProxy()).thenReturn(foo);
+
+ provider.writeTo(generator, event);
+
+ verify(event, atLeastOnce()).getThrowableProxy();
+ verifyNoInteractions(generator);
+ }
+}