Skip to content

Configurable exception pattern for log4j2 #177

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,11 @@ public class EcsLayout extends AbstractStringLayout {
private final String eventDataset;
private final boolean includeMarkers;
private final boolean includeOrigin;
private final PatternFormatter[] exceptionPatternFormatter;
private final ConcurrentMap<Class<? extends MultiformatMessage>, Boolean> supportsJson = new ConcurrentHashMap<Class<? extends MultiformatMessage>, Boolean>();

private EcsLayout(Configuration config, String serviceName, String serviceNodeName, String eventDataset, boolean includeMarkers,
KeyValuePair[] additionalFields, boolean includeOrigin, boolean stackTraceAsArray) {
KeyValuePair[] additionalFields, boolean includeOrigin, String exceptionPattern, boolean stackTraceAsArray) {
super(config, UTF_8, null, null);
this.serviceName = serviceName;
this.serviceNodeName = serviceNodeName;
Expand All @@ -96,6 +97,14 @@ private EcsLayout(Configuration config, String serviceName, String serviceNodeNa
.toArray(new PatternFormatter[0]);
}
}

if (exceptionPattern != null && !exceptionPattern.isEmpty()) {
exceptionPatternFormatter = PatternLayout.createPatternParser(config)
.parse(exceptionPattern)
.toArray(new PatternFormatter[0]);
} else {
exceptionPatternFormatter = null;
}
}

@PluginBuilderFactory
Expand Down Expand Up @@ -140,7 +149,7 @@ private StringBuilder toText(LogEvent event, StringBuilder builder, boolean gcFr
if (includeOrigin) {
EcsJsonSerializer.serializeOrigin(builder, event.getSource());
}
EcsJsonSerializer.serializeException(builder, event.getThrown(), stackTraceAsArray);
serializeException(builder, event);
EcsJsonSerializer.serializeObjectEnd(builder);
return builder;
}
Expand Down Expand Up @@ -325,6 +334,20 @@ private boolean supportsJson(MultiformatMessage message) {
return supportsJson;
}

private void serializeException(StringBuilder messageBuffer, LogEvent event) {
Throwable thrown = event.getThrown();
if (thrown != null) {
if (exceptionPatternFormatter != null) {
StringBuilder builder = EcsJsonSerializer.getMessageStringBuilder();
formatPattern(event, exceptionPatternFormatter, builder);
String stackTrace = builder.toString();
EcsJsonSerializer.serializeException(messageBuffer, thrown.getClass().getName(), thrown.getMessage(), stackTrace, stackTraceAsArray);
} else {
EcsJsonSerializer.serializeException(messageBuffer, thrown, stackTraceAsArray);
}
}
}

public static class Builder implements org.apache.logging.log4j.core.util.Builder<EcsLayout> {

@PluginConfiguration
Expand All @@ -337,6 +360,8 @@ public static class Builder implements org.apache.logging.log4j.core.util.Builde
private String eventDataset;
@PluginBuilderAttribute("includeMarkers")
private boolean includeMarkers = false;
@PluginBuilderAttribute("exceptionPattern")
private String exceptionPattern;
@PluginBuilderAttribute("stackTraceAsArray")
private boolean stackTraceAsArray = false;
@PluginElement("AdditionalField")
Expand Down Expand Up @@ -380,6 +405,14 @@ public boolean isIncludeOrigin() {
return includeOrigin;
}

public boolean isStackTraceAsArray() {
return stackTraceAsArray;
}

public String getExceptionPattern() {
return exceptionPattern;
}

/**
* Additional fields to set on each log event.
*
Expand Down Expand Up @@ -420,14 +453,15 @@ public EcsLayout.Builder setStackTraceAsArray(boolean stackTraceAsArray) {
return this;
}

public EcsLayout.Builder setExceptionPattern(String exceptionPattern) {
this.exceptionPattern = exceptionPattern;
return this;
}

@Override
public EcsLayout build() {
return new EcsLayout(getConfiguration(), serviceName, serviceNodeName, EcsJsonSerializer.computeEventDataset(eventDataset, serviceName),
includeMarkers, additionalFields, includeOrigin, stackTraceAsArray);
}

public boolean isStackTraceAsArray() {
return stackTraceAsArray;
includeMarkers, additionalFields, includeOrigin, exceptionPattern, stackTraceAsArray);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package co.elastic.logging.log4j2;

import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.pattern.ConverterKeys;
import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
import org.apache.logging.log4j.core.pattern.PatternConverter;

@Plugin(category = PatternConverter.CATEGORY, name = "CustomExceptionPatternConverter")
@ConverterKeys({"cEx"})
public class CustomExceptionPatternConverter extends LogEventPatternConverter {

public CustomExceptionPatternConverter(final String[] options) {
super("Custom", "custom");
}

public static CustomExceptionPatternConverter newInstance(final String[] options) {
return new CustomExceptionPatternConverter(options);
}


@Override
public void format(LogEvent event, StringBuilder toAppendTo) {
Throwable thrown = event.getThrown();
if (thrown != null) {
String message = thrown.getMessage();
if (message == null || message.isEmpty()) {
toAppendTo.append(thrown.getClass().getName())
.append('\n');
} else {
toAppendTo.append(thrown.getClass().getName())
.append(": ")
.append(message)
.append('\n');
}

toAppendTo.append("STACK_TRACE!");
}
}

@Override
public boolean handlesThrowable() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package co.elastic.logging.log4j2;

import com.fasterxml.jackson.databind.JsonNode;
import org.apache.logging.log4j.core.LoggerContext;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class EcsLayoutWithExceptionPatternTest extends Log4j2EcsLayoutTest {
@Override
protected EcsLayout.Builder configureLayout(LoggerContext context) {
return super.configureLayout(context)
.setExceptionPattern("%cEx");
}

@Test
void testLogException() throws Exception {
error("test", new RuntimeException("test"));
JsonNode log = getLastLogLine();
assertThat(log.get("log.level").textValue()).isIn("ERROR", "SEVERE");
assertThat(log.get("error.message").textValue()).isEqualTo("test");
assertThat(log.get("error.type").textValue()).isEqualTo(RuntimeException.class.getName());
assertThat(log.get("error.stack_trace").textValue()).isEqualTo("java.lang.RuntimeException: test\nSTACK_TRACE!");
}

@Test
void testLogExceptionNullMessage() throws Exception {
error("test", new RuntimeException());
JsonNode log = getLastLogLine();;
assertThat(log.get("error.type").textValue()).isEqualTo(RuntimeException.class.getName());
assertThat(log.get("error.message")).isNull();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assertThat(log.get("error.message")).isNull();
assertThat(log.get("error.message")).isNull();
assertThat(log.get("error.stack_trace").textValue()).isEqualTo("java.lang.RuntimeException\nSTACK_TRACE!");

(didn't actually test that, but based on code - we need to test this else branch as well)

assertThat(log.get("error.stack_trace").textValue()).isEqualTo("java.lang.RuntimeException\nSTACK_TRACE!");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package co.elastic.logging.log4j2;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import org.apache.logging.log4j.core.LoggerContext;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class EcsLayoutWithStackTraceAsArrayTest extends Log4j2EcsLayoutTest {

@Override
protected EcsLayout.Builder configureLayout(LoggerContext context) {
return super.configureLayout(context)
.setExceptionPattern("%rEx{4,filters(java.base,java.lang)}")
.setStackTraceAsArray(true);
}

@Test
void testLogException() throws Exception {
error("test", numberFormatException());
JsonNode log = getLastLogLine();
assertThat(log.get("log.level").textValue()).isIn("ERROR", "SEVERE");
assertThat(log.get("error.message").textValue()).isEqualTo("For input string: \"NOT_AN_INT\"");
assertThat(log.get("error.type").textValue()).isEqualTo(NumberFormatException.class.getName());
assertThat(log.get("error.stack_trace").isArray()).isTrue();
ArrayNode arrayNode = (ArrayNode) log.get("error.stack_trace");
assertThat(arrayNode.size()).isEqualTo(4);
assertThat(arrayNode.get(0).textValue()).isEqualTo("java.lang.NumberFormatException: For input string: \"NOT_AN_INT\"");
assertThat(arrayNode.get(1).textValue()).isEqualTo("\t... suppressed 3 lines");
assertThat(arrayNode.get(2).textValue()).startsWith("\tat co.elastic.logging.log4j2.EcsLayoutWithStackTraceAsArrayTest.numberFormatException");
assertThat(arrayNode.get(3).textValue()).startsWith("\tat co.elastic.logging.log4j2.EcsLayoutWithStackTraceAsArrayTest.testLogException");
}

private static Throwable numberFormatException() {
try {
Integer.parseInt("NOT_AN_INT");
return null;
} catch (Exception ex) {
return ex;
}
}

@Test
void testLogExceptionNullMessage() throws Exception {
error("test", new RuntimeException());
// skip validation that error.stack_trace is a string
JsonNode log = getLastLogLine();
assertThat(log.get("error.type").textValue()).isEqualTo(RuntimeException.class.getName());
assertThat(log.get("error.message")).isNull();
assertThat(log.get("error.stack_trace").isArray()).isTrue();
ArrayNode arrayNode = (ArrayNode) log.get("error.stack_trace");
assertThat(arrayNode.size()).isEqualTo(4);
assertThat(arrayNode.get(0).textValue()).isEqualTo("java.lang.RuntimeException");
assertThat(arrayNode.get(1).textValue()).startsWith("\tat co.elastic.logging.log4j2.EcsLayoutWithStackTraceAsArrayTest.testLogExceptionNullMessage");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@
import org.apache.logging.log4j.test.appender.ListAppender;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;

import static org.assertj.core.api.Assertions.assertThat;

class Log4j2EcsLayoutIntegrationTest extends AbstractLog4j2EcsLayoutTest {
@BeforeEach
void setUp() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,24 @@ void setUp() {
for (final Appender appender : root.getAppenders().values()) {
root.removeAppender(appender);
}
EcsLayout ecsLayout = EcsLayout.newBuilder()
.setConfiguration(ctx.getConfiguration())
EcsLayout ecsLayout = configureLayout(ctx)
.build();

listAppender = new ListAppender("ecs", null, ecsLayout, false, false);
listAppender.start();
root.addAppender(listAppender);
root.setLevel(Level.DEBUG);
}

protected EcsLayout.Builder configureLayout(LoggerContext context) {
return EcsLayout.newBuilder()
.setConfiguration(context.getConfiguration())
.setServiceName("test")
.setServiceNodeName("test-node")
.setIncludeMarkers(true)
.setIncludeOrigin(true)
.setEventDataset("testdataset")
.setExceptionPattern("%ex{4}")
.setAdditionalFields(new KeyValuePair[]{
new KeyValuePair("cluster.uuid", "9fe9134b-20b0-465e-acf9-8cc09ac9053b"),
new KeyValuePair("node.id", "${node.id}"),
Expand All @@ -80,13 +91,7 @@ void setUp() {
new KeyValuePair("emptyPattern", "%notEmpty{%invalidPattern}"),
new KeyValuePair("key1", "value1"),
new KeyValuePair("key2", "value2"),
})
.build();

listAppender = new ListAppender("ecs", null, ecsLayout, false, false);
listAppender.start();
root.addAppender(listAppender);
root.setLevel(Level.DEBUG);
});
}

@AfterEach
Expand Down
2 changes: 1 addition & 1 deletion log4j2-ecs-layout/src/test/resources/log4j2-test.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Appenders>
<List name="TestAppender">
<EcsLayout serviceName="test" serviceNodeName="test-node" includeMarkers="true" includeOrigin="true"
eventDataset="testdataset">
eventDataset="testdataset" exceptionPattern="%ex{4}">
<KeyValuePair key="cluster.uuid" value="9fe9134b-20b0-465e-acf9-8cc09ac9053b"/>
<KeyValuePair key="node.id" value="${node.id}"/>
<KeyValuePair key="empty" value="${empty}"/>
Expand Down