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.

+ + + 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.

- + + + 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.

+ + + 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); + } +}