Skip to content

Commit 05e5383

Browse files
authored
Configurable exception pattern for log4j2 (#177)
1 parent 775556e commit 05e5383

File tree

7 files changed

+195
-17
lines changed

7 files changed

+195
-17
lines changed

log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,11 @@ public class EcsLayout extends AbstractStringLayout {
7575
private final String eventDataset;
7676
private final boolean includeMarkers;
7777
private final boolean includeOrigin;
78+
private final PatternFormatter[] exceptionPatternFormatter;
7879
private final ConcurrentMap<Class<? extends MultiformatMessage>, Boolean> supportsJson = new ConcurrentHashMap<Class<? extends MultiformatMessage>, Boolean>();
7980

8081
private EcsLayout(Configuration config, String serviceName, String serviceNodeName, String eventDataset, boolean includeMarkers,
81-
KeyValuePair[] additionalFields, boolean includeOrigin, boolean stackTraceAsArray) {
82+
KeyValuePair[] additionalFields, boolean includeOrigin, String exceptionPattern, boolean stackTraceAsArray) {
8283
super(config, UTF_8, null, null);
8384
this.serviceName = serviceName;
8485
this.serviceNodeName = serviceNodeName;
@@ -96,6 +97,14 @@ private EcsLayout(Configuration config, String serviceName, String serviceNodeNa
9697
.toArray(new PatternFormatter[0]);
9798
}
9899
}
100+
101+
if (exceptionPattern != null && !exceptionPattern.isEmpty()) {
102+
exceptionPatternFormatter = PatternLayout.createPatternParser(config)
103+
.parse(exceptionPattern)
104+
.toArray(new PatternFormatter[0]);
105+
} else {
106+
exceptionPatternFormatter = null;
107+
}
99108
}
100109

101110
@PluginBuilderFactory
@@ -140,7 +149,7 @@ private StringBuilder toText(LogEvent event, StringBuilder builder, boolean gcFr
140149
if (includeOrigin) {
141150
EcsJsonSerializer.serializeOrigin(builder, event.getSource());
142151
}
143-
EcsJsonSerializer.serializeException(builder, event.getThrown(), stackTraceAsArray);
152+
serializeException(builder, event);
144153
EcsJsonSerializer.serializeObjectEnd(builder);
145154
return builder;
146155
}
@@ -325,6 +334,20 @@ private boolean supportsJson(MultiformatMessage message) {
325334
return supportsJson;
326335
}
327336

337+
private void serializeException(StringBuilder messageBuffer, LogEvent event) {
338+
Throwable thrown = event.getThrown();
339+
if (thrown != null) {
340+
if (exceptionPatternFormatter != null) {
341+
StringBuilder builder = EcsJsonSerializer.getMessageStringBuilder();
342+
formatPattern(event, exceptionPatternFormatter, builder);
343+
String stackTrace = builder.toString();
344+
EcsJsonSerializer.serializeException(messageBuffer, thrown.getClass().getName(), thrown.getMessage(), stackTrace, stackTraceAsArray);
345+
} else {
346+
EcsJsonSerializer.serializeException(messageBuffer, thrown, stackTraceAsArray);
347+
}
348+
}
349+
}
350+
328351
public static class Builder implements org.apache.logging.log4j.core.util.Builder<EcsLayout> {
329352

330353
@PluginConfiguration
@@ -337,6 +360,8 @@ public static class Builder implements org.apache.logging.log4j.core.util.Builde
337360
private String eventDataset;
338361
@PluginBuilderAttribute("includeMarkers")
339362
private boolean includeMarkers = false;
363+
@PluginBuilderAttribute("exceptionPattern")
364+
private String exceptionPattern;
340365
@PluginBuilderAttribute("stackTraceAsArray")
341366
private boolean stackTraceAsArray = false;
342367
@PluginElement("AdditionalField")
@@ -380,6 +405,14 @@ public boolean isIncludeOrigin() {
380405
return includeOrigin;
381406
}
382407

408+
public boolean isStackTraceAsArray() {
409+
return stackTraceAsArray;
410+
}
411+
412+
public String getExceptionPattern() {
413+
return exceptionPattern;
414+
}
415+
383416
/**
384417
* Additional fields to set on each log event.
385418
*
@@ -420,14 +453,15 @@ public EcsLayout.Builder setStackTraceAsArray(boolean stackTraceAsArray) {
420453
return this;
421454
}
422455

456+
public EcsLayout.Builder setExceptionPattern(String exceptionPattern) {
457+
this.exceptionPattern = exceptionPattern;
458+
return this;
459+
}
460+
423461
@Override
424462
public EcsLayout build() {
425463
return new EcsLayout(getConfiguration(), serviceName, serviceNodeName, EcsJsonSerializer.computeEventDataset(eventDataset, serviceName),
426-
includeMarkers, additionalFields, includeOrigin, stackTraceAsArray);
427-
}
428-
429-
public boolean isStackTraceAsArray() {
430-
return stackTraceAsArray;
464+
includeMarkers, additionalFields, includeOrigin, exceptionPattern, stackTraceAsArray);
431465
}
432466
}
433467
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package co.elastic.logging.log4j2;
2+
3+
import org.apache.logging.log4j.core.LogEvent;
4+
import org.apache.logging.log4j.core.config.plugins.Plugin;
5+
import org.apache.logging.log4j.core.pattern.ConverterKeys;
6+
import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
7+
import org.apache.logging.log4j.core.pattern.PatternConverter;
8+
9+
@Plugin(category = PatternConverter.CATEGORY, name = "CustomExceptionPatternConverter")
10+
@ConverterKeys({"cEx"})
11+
public class CustomExceptionPatternConverter extends LogEventPatternConverter {
12+
13+
public CustomExceptionPatternConverter(final String[] options) {
14+
super("Custom", "custom");
15+
}
16+
17+
public static CustomExceptionPatternConverter newInstance(final String[] options) {
18+
return new CustomExceptionPatternConverter(options);
19+
}
20+
21+
22+
@Override
23+
public void format(LogEvent event, StringBuilder toAppendTo) {
24+
Throwable thrown = event.getThrown();
25+
if (thrown != null) {
26+
String message = thrown.getMessage();
27+
if (message == null || message.isEmpty()) {
28+
toAppendTo.append(thrown.getClass().getName())
29+
.append('\n');
30+
} else {
31+
toAppendTo.append(thrown.getClass().getName())
32+
.append(": ")
33+
.append(message)
34+
.append('\n');
35+
}
36+
37+
toAppendTo.append("STACK_TRACE!");
38+
}
39+
}
40+
41+
@Override
42+
public boolean handlesThrowable() {
43+
return true;
44+
}
45+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package co.elastic.logging.log4j2;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import org.apache.logging.log4j.core.LoggerContext;
5+
import org.junit.jupiter.api.Test;
6+
7+
import static org.assertj.core.api.Assertions.assertThat;
8+
9+
public class EcsLayoutWithExceptionPatternTest extends Log4j2EcsLayoutTest {
10+
@Override
11+
protected EcsLayout.Builder configureLayout(LoggerContext context) {
12+
return super.configureLayout(context)
13+
.setExceptionPattern("%cEx");
14+
}
15+
16+
@Test
17+
void testLogException() throws Exception {
18+
error("test", new RuntimeException("test"));
19+
JsonNode log = getLastLogLine();
20+
assertThat(log.get("log.level").textValue()).isIn("ERROR", "SEVERE");
21+
assertThat(log.get("error.message").textValue()).isEqualTo("test");
22+
assertThat(log.get("error.type").textValue()).isEqualTo(RuntimeException.class.getName());
23+
assertThat(log.get("error.stack_trace").textValue()).isEqualTo("java.lang.RuntimeException: test\nSTACK_TRACE!");
24+
}
25+
26+
@Test
27+
void testLogExceptionNullMessage() throws Exception {
28+
error("test", new RuntimeException());
29+
JsonNode log = getLastLogLine();
30+
assertThat(log.get("error.type").textValue()).isEqualTo(RuntimeException.class.getName());
31+
assertThat(log.get("error.message")).isNull();
32+
assertThat(log.get("error.stack_trace").textValue()).isEqualTo("java.lang.RuntimeException\nSTACK_TRACE!");
33+
}
34+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package co.elastic.logging.log4j2;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.node.ArrayNode;
5+
import org.apache.logging.log4j.core.LoggerContext;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
public class EcsLayoutWithStackTraceAsArrayTest extends Log4j2EcsLayoutTest {
11+
12+
@Override
13+
protected EcsLayout.Builder configureLayout(LoggerContext context) {
14+
return super.configureLayout(context)
15+
.setExceptionPattern("%rEx{4,filters(java.base,java.lang)}")
16+
.setStackTraceAsArray(true);
17+
}
18+
19+
@Test
20+
void testLogException() throws Exception {
21+
error("test", numberFormatException());
22+
JsonNode log = getLastLogLine();
23+
assertThat(log.get("log.level").textValue()).isIn("ERROR", "SEVERE");
24+
assertThat(log.get("error.message").textValue()).isEqualTo("For input string: \"NOT_AN_INT\"");
25+
assertThat(log.get("error.type").textValue()).isEqualTo(NumberFormatException.class.getName());
26+
assertThat(log.get("error.stack_trace").isArray()).isTrue();
27+
ArrayNode arrayNode = (ArrayNode) log.get("error.stack_trace");
28+
assertThat(arrayNode.size()).isEqualTo(4);
29+
assertThat(arrayNode.get(0).textValue()).isEqualTo("java.lang.NumberFormatException: For input string: \"NOT_AN_INT\"");
30+
assertThat(arrayNode.get(1).textValue()).startsWith("\t... suppressed");
31+
assertThat(arrayNode.get(2).textValue()).startsWith("\tat co.elastic.logging.log4j2.EcsLayoutWithStackTraceAsArrayTest.numberFormatException");
32+
assertThat(arrayNode.get(3).textValue()).startsWith("\tat co.elastic.logging.log4j2.EcsLayoutWithStackTraceAsArrayTest.testLogException");
33+
}
34+
35+
private static Throwable numberFormatException() {
36+
try {
37+
Integer.parseInt("NOT_AN_INT");
38+
return null;
39+
} catch (Exception ex) {
40+
return ex;
41+
}
42+
}
43+
44+
@Test
45+
void testLogExceptionNullMessage() throws Exception {
46+
error("test", new RuntimeException());
47+
// skip validation that error.stack_trace is a string
48+
JsonNode log = getLastLogLine();
49+
assertThat(log.get("error.type").textValue()).isEqualTo(RuntimeException.class.getName());
50+
assertThat(log.get("error.message")).isNull();
51+
assertThat(log.get("error.stack_trace").isArray()).isTrue();
52+
ArrayNode arrayNode = (ArrayNode) log.get("error.stack_trace");
53+
assertThat(arrayNode.size()).isEqualTo(4);
54+
assertThat(arrayNode.get(0).textValue()).isEqualTo("java.lang.RuntimeException");
55+
assertThat(arrayNode.get(1).textValue()).startsWith("\tat co.elastic.logging.log4j2.EcsLayoutWithStackTraceAsArrayTest.testLogExceptionNullMessage");
56+
}
57+
}

log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/Log4j2EcsLayoutIntegrationTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,12 @@
2929
import org.apache.logging.log4j.test.appender.ListAppender;
3030
import org.junit.jupiter.api.AfterEach;
3131
import org.junit.jupiter.api.BeforeEach;
32+
import org.junit.jupiter.api.Test;
3233

3334
import java.io.IOException;
3435

36+
import static org.assertj.core.api.Assertions.assertThat;
37+
3538
class Log4j2EcsLayoutIntegrationTest extends AbstractLog4j2EcsLayoutTest {
3639
@BeforeEach
3740
void setUp() {

log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/Log4j2EcsLayoutTest.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,24 @@ void setUp() {
6464
for (final Appender appender : root.getAppenders().values()) {
6565
root.removeAppender(appender);
6666
}
67-
EcsLayout ecsLayout = EcsLayout.newBuilder()
68-
.setConfiguration(ctx.getConfiguration())
67+
EcsLayout ecsLayout = configureLayout(ctx)
68+
.build();
69+
70+
listAppender = new ListAppender("ecs", null, ecsLayout, false, false);
71+
listAppender.start();
72+
root.addAppender(listAppender);
73+
root.setLevel(Level.DEBUG);
74+
}
75+
76+
protected EcsLayout.Builder configureLayout(LoggerContext context) {
77+
return EcsLayout.newBuilder()
78+
.setConfiguration(context.getConfiguration())
6979
.setServiceName("test")
7080
.setServiceNodeName("test-node")
7181
.setIncludeMarkers(true)
7282
.setIncludeOrigin(true)
7383
.setEventDataset("testdataset")
84+
.setExceptionPattern("%ex{4}")
7485
.setAdditionalFields(new KeyValuePair[]{
7586
new KeyValuePair("cluster.uuid", "9fe9134b-20b0-465e-acf9-8cc09ac9053b"),
7687
new KeyValuePair("node.id", "${node.id}"),
@@ -80,13 +91,7 @@ void setUp() {
8091
new KeyValuePair("emptyPattern", "%notEmpty{%invalidPattern}"),
8192
new KeyValuePair("key1", "value1"),
8293
new KeyValuePair("key2", "value2"),
83-
})
84-
.build();
85-
86-
listAppender = new ListAppender("ecs", null, ecsLayout, false, false);
87-
listAppender.start();
88-
root.addAppender(listAppender);
89-
root.setLevel(Level.DEBUG);
94+
});
9095
}
9196

9297
@AfterEach

log4j2-ecs-layout/src/test/resources/log4j2-test.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<Appenders>
77
<List name="TestAppender">
88
<EcsLayout serviceName="test" serviceNodeName="test-node" includeMarkers="true" includeOrigin="true"
9-
eventDataset="testdataset">
9+
eventDataset="testdataset" exceptionPattern="%ex{4}">
1010
<KeyValuePair key="cluster.uuid" value="9fe9134b-20b0-465e-acf9-8cc09ac9053b"/>
1111
<KeyValuePair key="node.id" value="${node.id}"/>
1212
<KeyValuePair key="empty" value="${empty}"/>

0 commit comments

Comments
 (0)