Skip to content

Commit

Permalink
Merge PR #743 Add new JsonProviders for exception messages
Browse files Browse the repository at this point in the history
* New providers `throwableMessage` and `throwableRootCauseMessage`
* New cyclic reference safe algorithm to determine a throwable's root
  cause
* Share that algorithm between `throwableRootCauseClassName` and
  `throwableRootCauseMessage`
  • Loading branch information
twz123 authored and philsttr committed Apr 2, 2022
1 parent c89549f commit 69f5ce8
Show file tree
Hide file tree
Showing 10 changed files with 404 additions and 10 deletions.
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2160,15 +2160,31 @@ The provider name is the xml element name to use when configuring. Each provider
</ul>
</td>
</tr>
<tr>
<td valign="top"><tt>throwableMessage</tt></td>
<td><p>(Only if a throwable was logged) Outputs a field that contains the message of the thrown Throwable.</p>
<ul>
<li><tt>fieldName</tt> - Output field name (<tt>throwable_message</tt>)</li>
</ul>
</td>
</tr>
<tr>
<td valign="top"><tt>throwableRootCauseClassName</tt></td>
<td><p>(Only if a throwable was logged) Outputs a field that contains the class name of the root cause of the thrown Throwable.</p>
<td><p>(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.</p>
<ul>
<li><tt>fieldName</tt> - Output field name (<tt>throwable_root_cause_class</tt>)</li>
<li><tt>useSimpleClassName</tt> - When true, the throwable's simple class name will be used. When false, the fully qualified class name will be used. (<tt>true</tt>)</li>
</ul>
</td>
</tr>
</tr>
<tr>
<td valign="top"><tt>throwableRootCauseMessage</tt></td>
<td><p>(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.</p>
<ul>
<li><tt>fieldName</tt> - Output field name (<tt>throwable_root_cause_message</tt>)</li>
</ul>
</td>
</tr>
</tbody>
</table>

Expand Down
Original file line number Diff line number Diff line change
@@ -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<ILoggingEvent> {

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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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}.
*
* @param throwable the throwable for which to find the root cause
* @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;
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Loading

0 comments on commit 69f5ce8

Please sign in to comment.