Skip to content

Commit 8b87a51

Browse files
Implement ability to configure a throwable converter (#127)
1 parent 7168239 commit 8b87a51

File tree

7 files changed

+280
-36
lines changed

7 files changed

+280
-36
lines changed

ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
* the Apache License, Version 2.0 (the "License"); you may
1212
* not use this file except in compliance with the License.
1313
* You may obtain a copy of the License at
14-
*
14+
*
1515
* http://www.apache.org/licenses/LICENSE-2.0
16-
*
16+
*
1717
* Unless required by applicable law or agreed to in writing,
1818
* software distributed under the License is distributed on an
1919
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -34,7 +34,7 @@ public class EcsJsonSerializer {
3434

3535
private static final TimestampSerializer TIMESTAMP_SERIALIZER = new TimestampSerializer();
3636
private static final ThreadLocal<StringBuilder> messageStringBuilder = new ThreadLocal<StringBuilder>();
37-
private static final String NEW_LINE = System.getProperty("line.separator");
37+
private static final String NEW_LINE = System.getProperty("line.separator");
3838
private static final Pattern NEW_LINE_PATTERN = Pattern.compile("\\n");
3939

4040
public static CharSequence toNullSafeString(final CharSequence s) {
@@ -79,6 +79,7 @@ public static void serializeThreadId(StringBuilder builder, long threadId) {
7979
builder.append(threadId);
8080
builder.append(",");
8181
}
82+
8283
public static void serializeFormattedMessage(StringBuilder builder, String message) {
8384
builder.append("\"message\":\"");
8485
JsonUtils.quoteAsString(message, builder);
@@ -214,14 +215,15 @@ public static void serializeException(StringBuilder builder, String exceptionCla
214215
builder.append("\"error.type\":\"");
215216
JsonUtils.quoteAsString(exceptionClassName, builder);
216217
builder.append("\",");
217-
builder.append("\"error.message\":\"");
218-
JsonUtils.quoteAsString(exceptionMessage, builder);
219-
builder.append("\",");
218+
219+
if (exceptionMessage != null) {
220+
builder.append("\"error.message\":\"");
221+
JsonUtils.quoteAsString(exceptionMessage, builder);
222+
builder.append("\",");
223+
}
220224
if (stackTraceAsArray) {
221-
builder.append("\"error.stack_trace\":[").append(NEW_LINE);
222-
for (String line : NEW_LINE_PATTERN.split(stackTrace)) {
223-
appendQuoted(builder, line);
224-
}
225+
builder.append("\"error.stack_trace\":[");
226+
formatStackTraceAsArray(builder, stackTrace);
225227
builder.append("]");
226228
} else {
227229
builder.append("\"error.stack_trace\":\"");
@@ -230,12 +232,6 @@ public static void serializeException(StringBuilder builder, String exceptionCla
230232
}
231233
}
232234

233-
private static void appendQuoted(StringBuilder builder, CharSequence content) {
234-
builder.append('"');
235-
JsonUtils.quoteAsString(content, builder);
236-
builder.append('"');
237-
}
238-
239235
private static CharSequence formatThrowable(final Throwable throwable) {
240236
StringBuilder buffer = getMessageStringBuilder();
241237
final PrintWriter pw = new PrintWriter(new StringBuilderWriter(buffer));
@@ -262,6 +258,18 @@ public void println() {
262258
removeIfEndsWith(jsonBuilder, ",");
263259
}
264260

261+
private static void formatStackTraceAsArray(StringBuilder builder, String stackTrace) {
262+
builder.append(NEW_LINE);
263+
for (String line : NEW_LINE_PATTERN.split(stackTrace)) {
264+
builder.append("\t\"");
265+
JsonUtils.quoteAsString(line, builder);
266+
builder.append("\",");
267+
builder.append(NEW_LINE);
268+
}
269+
removeIfEndsWith(builder, NEW_LINE);
270+
removeIfEndsWith(builder, ",");
271+
}
272+
265273
public static void removeIfEndsWith(StringBuilder sb, String ending) {
266274
if (endsWith(sb, ending)) {
267275
sb.setLength(sb.length() - ending.length());

ecs-logging-core/src/test/java/co/elastic/logging/AbstractEcsLoggingTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
public abstract class AbstractEcsLoggingTest {
4646

4747
protected ObjectMapper objectMapper = new ObjectMapper();
48-
private JsonNode spec;
48+
protected JsonNode spec;
4949

5050
@BeforeEach
5151
final void setUpSpec() throws Exception {

ecs-logging-core/src/test/java/co/elastic/logging/EcsJsonSerializerTest.java

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
* the Apache License, Version 2.0 (the "License"); you may
1212
* not use this file except in compliance with the License.
1313
* You may obtain a copy of the License at
14-
*
14+
*
1515
* http://www.apache.org/licenses/LICENSE-2.0
16-
*
16+
*
1717
* Unless required by applicable law or agreed to in writing,
1818
* software distributed under the License is distributed on an
1919
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -40,20 +40,25 @@
4040

4141
class EcsJsonSerializerTest {
4242

43+
private static final String ERROR_TYPE = "error.type";
44+
private static final String ERROR_STACK_TRACE = "error.stack_trace";
45+
private static final String ERROR_MESSAGE = "error.message";
46+
private ObjectMapper objectMapper = new ObjectMapper();
47+
4348
@Test
4449
void serializeExceptionAsString() throws IOException {
4550
Exception exception = new Exception("foo");
4651
StringBuilder jsonBuilder = new StringBuilder();
4752
jsonBuilder.append('{');
4853
EcsJsonSerializer.serializeException(jsonBuilder, exception, false);
4954
jsonBuilder.append('}');
50-
JsonNode jsonNode = new ObjectMapper().readTree(jsonBuilder.toString());
55+
JsonNode jsonNode = objectMapper.readTree(jsonBuilder.toString());
5156

52-
assertThat(jsonNode.get("error.type").textValue()).isEqualTo(exception.getClass().getName());
53-
assertThat(jsonNode.get("error.message").textValue()).isEqualTo("foo");
57+
assertThat(jsonNode.get(ERROR_TYPE).textValue()).isEqualTo(exception.getClass().getName());
58+
assertThat(jsonNode.get(ERROR_MESSAGE).textValue()).isEqualTo("foo");
5459
StringWriter stringWriter = new StringWriter();
5560
exception.printStackTrace(new PrintWriter(stringWriter));
56-
assertThat(jsonNode.get("error.stack_trace").textValue()).isEqualTo(stringWriter.toString());
61+
assertThat(jsonNode.get(ERROR_STACK_TRACE).textValue()).isEqualTo(stringWriter.toString());
5762
}
5863

5964
@Test
@@ -102,13 +107,13 @@ void serializeExceptionAsArray() throws IOException {
102107
EcsJsonSerializer.serializeException(jsonBuilder, exception, true);
103108
jsonBuilder.append('}');
104109
System.out.println(jsonBuilder);
105-
JsonNode jsonNode = new ObjectMapper().readTree(jsonBuilder.toString());
110+
JsonNode jsonNode = objectMapper.readTree(jsonBuilder.toString());
106111

107-
assertThat(jsonNode.get("error.type").textValue()).isEqualTo(exception.getClass().getName());
108-
assertThat(jsonNode.get("error.message").textValue()).isEqualTo("foo");
112+
assertThat(jsonNode.get(ERROR_TYPE).textValue()).isEqualTo(exception.getClass().getName());
113+
assertThat(jsonNode.get(ERROR_MESSAGE).textValue()).isEqualTo("foo");
109114
StringWriter stringWriter = new StringWriter();
110115
exception.printStackTrace(new PrintWriter(stringWriter));
111-
assertThat(StreamSupport.stream(jsonNode.get("error.stack_trace").spliterator(), false)
116+
assertThat(StreamSupport.stream(jsonNode.get(ERROR_STACK_TRACE).spliterator(), false)
112117
.map(JsonNode::textValue)
113118
.collect(Collectors.joining(System.lineSeparator(), "", System.lineSeparator())))
114119
.isEqualTo(stringWriter.toString());
@@ -121,6 +126,48 @@ void testRemoveIfEndsWith() {
121126
assertRemoveIfEndsWith("barfoo", "foo", "bar");
122127
}
123128

129+
@Test
130+
void serializeException() throws JsonProcessingException {
131+
StringBuilder jsonBuilder = new StringBuilder();
132+
jsonBuilder.append('{');
133+
EcsJsonSerializer.serializeException(jsonBuilder, "className", "message", "stacktrace\ncaused by error", false);
134+
jsonBuilder.append('}');
135+
136+
JsonNode jsonNode = objectMapper.readTree(jsonBuilder.toString());
137+
assertThat(jsonNode.get(ERROR_TYPE).textValue()).isEqualTo("className");
138+
assertThat(jsonNode.get(ERROR_STACK_TRACE).isArray()).isFalse();
139+
assertThat(jsonNode.get(ERROR_STACK_TRACE).textValue()).isEqualTo("stacktrace\ncaused by error");
140+
assertThat(jsonNode.get(ERROR_MESSAGE).textValue()).isEqualTo("message");
141+
}
142+
143+
@Test
144+
void serializeExceptionWithStackTraceAsArray() throws JsonProcessingException {
145+
StringBuilder jsonBuilder = new StringBuilder();
146+
jsonBuilder.append('{');
147+
EcsJsonSerializer.serializeException(jsonBuilder, "className", "message", "stacktrace\ncaused by error", true);
148+
jsonBuilder.append('}');
149+
150+
JsonNode jsonNode = objectMapper.readTree(jsonBuilder.toString());
151+
assertThat(jsonNode.get(ERROR_TYPE).textValue()).isEqualTo("className");
152+
assertThat(jsonNode.get(ERROR_STACK_TRACE).isArray()).isTrue();
153+
assertThat(jsonNode.get(ERROR_STACK_TRACE).get(0).textValue()).isEqualTo("stacktrace");
154+
assertThat(jsonNode.get(ERROR_STACK_TRACE).get(1).textValue()).isEqualTo("caused by error");
155+
assertThat(jsonNode.get(ERROR_MESSAGE).textValue()).isEqualTo("message");
156+
}
157+
158+
@Test
159+
void serializeExceptionWithNullMessage() throws JsonProcessingException {
160+
StringBuilder jsonBuilder = new StringBuilder();
161+
jsonBuilder.append('{');
162+
EcsJsonSerializer.serializeException(jsonBuilder, "className", null, "stacktrace", false);
163+
jsonBuilder.append('}');
164+
165+
JsonNode jsonNode = objectMapper.readTree(jsonBuilder.toString());
166+
assertThat(jsonNode.get(ERROR_TYPE).textValue()).isEqualTo("className");
167+
assertThat(jsonNode.get(ERROR_STACK_TRACE).textValue()).isEqualTo("stacktrace");
168+
assertThat(jsonNode.get(ERROR_MESSAGE)).isNull();
169+
}
170+
124171
private void assertRemoveIfEndsWith(String builder, String ending, String expected) {
125172
StringBuilder sb = new StringBuilder(builder);
126173
EcsJsonSerializer.removeIfEndsWith(sb, ending);

logback-ecs-encoder/src/main/java/co/elastic/logging/logback/EcsEncoder.java

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
* the Apache License, Version 2.0 (the "License"); you may
1212
* not use this file except in compliance with the License.
1313
* You may obtain a copy of the License at
14-
*
14+
*
1515
* http://www.apache.org/licenses/LICENSE-2.0
16-
*
16+
*
1717
* Unless required by applicable law or agreed to in writing,
1818
* software distributed under the License is distributed on an
1919
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -24,13 +24,14 @@
2424
*/
2525
package co.elastic.logging.logback;
2626

27+
import ch.qos.logback.classic.pattern.ThrowableHandlingConverter;
2728
import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
2829
import ch.qos.logback.classic.spi.ILoggingEvent;
2930
import ch.qos.logback.classic.spi.IThrowableProxy;
3031
import ch.qos.logback.classic.spi.ThrowableProxy;
3132
import ch.qos.logback.core.encoder.EncoderBase;
32-
import co.elastic.logging.EcsJsonSerializer;
3333
import co.elastic.logging.AdditionalField;
34+
import co.elastic.logging.EcsJsonSerializer;
3435
import org.slf4j.Marker;
3536

3637
import java.io.IOException;
@@ -48,7 +49,8 @@ public class EcsEncoder extends EncoderBase<ILoggingEvent> {
4849
private String serviceNodeName;
4950
private String eventDataset;
5051
private boolean includeMarkers = false;
51-
private ThrowableProxyConverter throwableProxyConverter;
52+
private ThrowableHandlingConverter throwableConverter = null;
53+
private final ThrowableProxyConverter throwableProxyConverter = new ThrowableProxyConverter();
5254
private boolean includeOrigin;
5355
private final List<AdditionalField> additionalFields = new ArrayList<AdditionalField>();
5456
private OutputStream os;
@@ -61,10 +63,13 @@ public byte[] headerBytes() {
6163
@Override
6264
public void start() {
6365
super.start();
64-
throwableProxyConverter = new ThrowableProxyConverter();
6566
throwableProxyConverter.start();
67+
if (throwableConverter != null) {
68+
throwableConverter.start();
69+
}
6670
eventDataset = EcsJsonSerializer.computeEventDataset(eventDataset, serviceName);
6771
}
72+
6873
/**
6974
* This method has been removed in logback 1.2.
7075
* To make this lib backwards compatible with logback 1.1 we have implement this method.
@@ -117,10 +122,14 @@ public byte[] encode(ILoggingEvent event) {
117122
// Allow subclasses to add custom fields. Calling this before the throwable serialization so we don't need to check for presence/absence of an ending comma.
118123
addCustomFields(event, builder);
119124
IThrowableProxy throwableProxy = event.getThrowableProxy();
120-
if (throwableProxy instanceof ThrowableProxy) {
121-
EcsJsonSerializer.serializeException(builder, ((ThrowableProxy) throwableProxy).getThrowable(), stackTraceAsArray);
122-
} else if (throwableProxy != null) {
123-
EcsJsonSerializer.serializeException(builder, throwableProxy.getClassName(), throwableProxy.getMessage(), throwableProxyConverter.convert(event), stackTraceAsArray);
125+
if (throwableProxy != null) {
126+
if (throwableConverter != null) {
127+
EcsJsonSerializer.serializeException(builder, throwableProxy.getClassName(), throwableProxy.getMessage(), throwableConverter.convert(event), stackTraceAsArray);
128+
} else if (throwableProxy instanceof ThrowableProxy) {
129+
EcsJsonSerializer.serializeException(builder, ((ThrowableProxy) throwableProxy).getThrowable(), stackTraceAsArray);
130+
} else {
131+
EcsJsonSerializer.serializeException(builder, throwableProxy.getClassName(), throwableProxy.getMessage(), throwableProxyConverter.convert(event), stackTraceAsArray);
132+
}
124133
}
125134
EcsJsonSerializer.serializeObjectEnd(builder);
126135
// all these allocations kinda hurt
@@ -186,4 +195,7 @@ public void setEventDataset(String eventDataset) {
186195
this.eventDataset = eventDataset;
187196
}
188197

198+
public void setThrowableConverter(ThrowableHandlingConverter throwableConverter) {
199+
this.throwableConverter = throwableConverter;
200+
}
189201
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*-
2+
* #%L
3+
* Java ECS logging
4+
* %%
5+
* Copyright (C) 2019 - 2021 Elastic and contributors
6+
* %%
7+
* Licensed to Elasticsearch B.V. under one or more contributor
8+
* license agreements. See the NOTICE file distributed with
9+
* this work for additional information regarding copyright
10+
* ownership. Elasticsearch B.V. licenses this file to you under
11+
* the Apache License, Version 2.0 (the "License"); you may
12+
* not use this file except in compliance with the License.
13+
* You may obtain a copy of the License at
14+
*
15+
* http://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* Unless required by applicable law or agreed to in writing,
18+
* software distributed under the License is distributed on an
19+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20+
* KIND, either express or implied. See the License for the
21+
* specific language governing permissions and limitations
22+
* under the License.
23+
* #L%
24+
*/
25+
package co.elastic.logging.logback;
26+
27+
import ch.qos.logback.classic.LoggerContext;
28+
import ch.qos.logback.classic.util.ContextInitializer;
29+
import ch.qos.logback.core.joran.spi.JoranException;
30+
import com.fasterxml.jackson.databind.JsonNode;
31+
import org.junit.jupiter.api.BeforeEach;
32+
import org.junit.jupiter.api.Test;
33+
34+
import java.io.IOException;
35+
36+
import static org.assertj.core.api.Assertions.assertThat;
37+
38+
public class EcsEncoderWithCustomThrowableConverterIntegrationTest extends AbstractEcsEncoderTest {
39+
private OutputStreamAppender appender;
40+
41+
@BeforeEach
42+
void setUp() throws JoranException {
43+
LoggerContext context = new LoggerContext();
44+
ContextInitializer contextInitializer = new ContextInitializer(context);
45+
contextInitializer.configureByResource(this.getClass().getResource("/logback-config-with-nop-throwable-converter.xml"));
46+
logger = context.getLogger("root");
47+
appender = (OutputStreamAppender) logger.getAppender("out");
48+
}
49+
50+
@Override
51+
public JsonNode getLastLogLine() throws IOException {
52+
return objectMapper.readTree(appender.getBytes());
53+
}
54+
55+
@Test
56+
void testLogException() throws Exception {
57+
error("test", new RuntimeException("test"));
58+
JsonNode log = getAndValidateLastLogLine();
59+
assertThat(log.get("log.level").textValue()).isIn("ERROR", "SEVERE");
60+
assertThat(log.get("error.message").textValue()).isEqualTo("test");
61+
assertThat(log.get("error.type").textValue()).isEqualTo(RuntimeException.class.getName());
62+
assertThat(log.get("error.stack_trace").textValue()).contains("");
63+
}
64+
}

0 commit comments

Comments
 (0)