Skip to content

Commit 43f3803

Browse files
authored
Serialize stack traces in error.stack_trace (#28)
1 parent 7e441fb commit 43f3803

File tree

14 files changed

+368
-78
lines changed

14 files changed

+368
-78
lines changed

README.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ Example:
2020
{"@timestamp":"2019-08-06T12:09:12.375Z", "log.level": "INFO", "message":"Tomcat started on port(s): 8080 (http) with context path ''", "service.name":"spring-petclinic","process.thread.name":"restartedMain","log.logger":"org.springframework.boot.web.embedded.tomcat.TomcatWebServer"}
2121
{"@timestamp":"2019-08-06T12:09:12.379Z", "log.level": "INFO", "message":"Started PetClinicApplication in 7.095 seconds (JVM running for 9.082)", "service.name":"spring-petclinic","process.thread.name":"restartedMain","log.logger":"org.springframework.samples.petclinic.PetClinicApplication"}
2222
{"@timestamp":"2019-08-06T14:08:40.199Z", "log.level":"DEBUG", "message":"init find form", "service.name":"spring-petclinic","process.thread.name":"http-nio-8080-exec-8","log.logger":"org.springframework.samples.petclinic.owner.OwnerController","transaction.id":"28b7fb8d5aba51f1","trace.id":"2869b25b5469590610fea49ac04af7da"}
23+
{"@timestamp":"2019-09-17T13:16:48.038Z", "log.level":"ERROR", "message":"Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: Expected: controller used to showcase what happens when an exception is thrown] with root cause", "process.thread.name":"http-nio-8080-exec-1","log.logger":"org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[dispatcherServlet]","log.origin":{"file":"DirectJDKLog.java","function":"log","line":175},"error.code":"java.lang.RuntimeException","error.message":"Expected: controller used to showcase what happens when an exception is thrown","error.stack_trace":[
24+
"java.lang.RuntimeException: Expected: controller used to showcase what happens when an exception is thrown",
25+
"\tat org.springframework.samples.petclinic.system.CrashController.triggerException(CrashController.java:33)",
26+
"\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)",
27+
"\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)",
28+
"\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)",
29+
"\tat java.lang.reflect.Method.invoke(Method.java:498)",
30+
"\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)",
31+
"\tat java.lang.Thread.run(Thread.java:748)"]}
2332
```
2433

2534
## Why ECS logging?
@@ -32,6 +41,7 @@ Example:
3241
The log4j2 `EcsLayout` does not allocate any memory (unless the log event contains an `Exception`)
3342
* Decently human-readable JSON structure \
3443
The first three fields are always `@timestamp`, `log.level` and `message`.
44+
It's also possible to format stack traces so that each element is rendered in a new line.
3545
* Use the Kibana [Logs UI](https://www.elastic.co/guide/en/kibana/7.3/xpack-logs.html) without additional configuration \
3646
As this library adheres to [ECS](https://www.elastic.co/guide/en/ecs/current/ecs-reference.html), the Logs UI knows which fields to show
3747
* Out-of-the-box correlation of logs and traces via the Logs UI and APM UI when using the [Elastic APM Java agent](https://www.elastic.co/guide/en/apm/agent/java/current/index.html)
@@ -69,10 +79,12 @@ We recommend using this library to log into a JSON log file and let Filebeat sen
6979
|ECS field | Log4j2 API |
7080
|----------|-------------|
7181
|[`@timestamp`](https://www.elastic.co/guide/en/ecs/current/ecs-base.html) | [`LogEvent#getTimeMillis()`](https://logging.apache.org/log4j/log4j-2.3/log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getTimeMillis()) |
72-
| [`log.level`](https://www.elastic.co/guide/en/ecs/current/ecs-log.html) | [`LogEvent#getLevel()`](https://logging.apache.org/log4j/log4j-2.3/log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getLevel()) |
73-
|[`log.logger`](https://www.elastic.co/guide/en/ecs/current/ecs-log.html)|[`LogEvent#getLoggerName(`](https://logging.apache.org/log4j/log4j-2.3/log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getLoggerName())|
82+
|[`log.level`](https://www.elastic.co/guide/en/ecs/current/ecs-log.html) | [`LogEvent#getLevel()`](https://logging.apache.org/log4j/log4j-2.3/log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getLevel()) |
83+
|[`log.logger`](https://www.elastic.co/guide/en/ecs/current/ecs-log.html)|[`LogEvent#getLoggerName()`](https://logging.apache.org/log4j/log4j-2.3/log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getLoggerName())|
7484
|[`message`](https://www.elastic.co/guide/en/ecs/current/ecs-base.html)|[`LogEvent#getMessage()`](https://logging.apache.org/log4j/log4j-2.3/log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getMessage())|
75-
|[`message`](https://www.elastic.co/guide/en/ecs/current/ecs-base.html)|[`LogEvent#getThrown()`](https://logging.apache.org/log4j/log4j-2.3/log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getThrown())|
85+
|[`error.code`](https://www.elastic.co/guide/en/ecs/current/ecs-error.html)|[`Throwable#getClass()`](https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#getClass())|
86+
|[`error.message`](https://www.elastic.co/guide/en/ecs/current/ecs-error.html)|[`Throwable#getStackTrace()`](https://docs.oracle.com/javase/7/docs/api/java/lang/Throwable.html#getMessage())|
87+
|[`error.stack_trace`](https://www.elastic.co/guide/en/ecs/current/ecs-error.html)|[`Throwable#getStackTrace()`](https://docs.oracle.com/javase/7/docs/api/java/lang/Throwable.html#getStackTrace())|
7688
|[`process.thread.name`](https://www.elastic.co/guide/en/ecs/current/ecs-process.html)|[`LogEvent#getThreadName()`](https://logging.apache.org/log4j/log4j-2.3/log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getThreadName()) |
7789
|[`labels`](https://www.elastic.co/guide/en/ecs/current/ecs-base.html)|[`LogEvent#getContextMap()`](https://logging.apache.org/log4j/log4j-2.3/log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getContextMap())|
7890
|[`tags`](https://www.elastic.co/guide/en/ecs/current/ecs-base.html)|[`LogEvent#getContextStack()`](https://logging.apache.org/log4j/log4j-2.3/log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getContextStack())|
@@ -123,3 +135,33 @@ For more information, check the [Filebeat documentation](https://www.elastic.co/
123135
- Set `Output type` to `Elasticsearch`
124136
- Configure the `hosts`
125137
- For secured Elasticsearch deployments (like Elastic cloud) set `Username` and `Password`
138+
139+
#### When `stackTraceAsArray` is enabled
140+
141+
Filebeat can normally only decoding JSON if there is one JSON object per line.
142+
When `stackTraceAsArray` is enabled, there will be a new line for each stack trace element which improves readability.
143+
But when combining the multiline settings with a `decode_json_fields` we can also handle multi-line JSON.
144+
145+
```yaml
146+
filebeat.inputs:
147+
- type: log
148+
paths: /path/to/logs.json
149+
multiline.pattern: '^{'
150+
multiline.negate: true
151+
multiline.match: after
152+
processors:
153+
- decode_json_fields:
154+
fields: message
155+
target: ""
156+
overwrite_keys: true
157+
# flattens the array to a single string
158+
- script:
159+
when:
160+
has_fields: ['error.stack_trace']
161+
lang: javascript
162+
id: my_filter
163+
source: >
164+
function process(event) {
165+
event.Put("error.stack_trace", event.Get("error.stack_trace").join("\n"));
166+
}
167+
```

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

Lines changed: 150 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
package co.elastic.logging;
2626

2727
import java.io.PrintWriter;
28-
import java.io.StringWriter;
28+
import java.io.Writer;
2929
import java.util.Arrays;
3030
import java.util.List;
3131
import java.util.Map;
@@ -35,6 +35,8 @@ public class EcsJsonSerializer {
3535

3636
public static final List<String> DEFAULT_TOP_LEVEL_LABELS = Arrays.asList("trace.id", "transaction.id", "span.id", "error.id", "service.name");
3737
private static final TimestampSerializer TIMESTAMP_SERIALIZER = new TimestampSerializer();
38+
private static final ThreadLocal<StringBuilder> messageStringBuilder = new ThreadLocal<StringBuilder>();
39+
private static final String NEW_LINE = System.getProperty("line.separator");
3840

3941
public static CharSequence toNullSafeString(final CharSequence s) {
4042
return s == null ? "" : s;
@@ -48,8 +50,7 @@ public static void serializeObjectStart(StringBuilder builder, long timeMillis)
4850
}
4951

5052
public static void serializeObjectEnd(StringBuilder builder) {
51-
// last char is always a comma (,)
52-
builder.setLength(builder.length() - 1);
53+
removeIfEndsWith(builder, ",");
5354
builder.append('}');
5455
builder.append('\n');
5556
}
@@ -68,13 +69,9 @@ public static void serializeThreadName(StringBuilder builder, String threadName)
6869
}
6970
}
7071

71-
public static void serializeFormattedMessage(StringBuilder builder, String message, Throwable t) {
72+
public static void serializeFormattedMessage(StringBuilder builder, String message) {
7273
builder.append("\"message\":\"");
7374
JsonUtils.quoteAsString(message, builder);
74-
if (t != null) {
75-
builder.append("\\n");
76-
JsonUtils.quoteAsString(formatThrowable(t), builder);
77-
}
7875
builder.append("\", ");
7976
}
8077

@@ -138,22 +135,162 @@ public static void serializeLabels(StringBuilder builder, Map<String, ?> labels,
138135
}
139136
}
140137

141-
public static void serializeException(StringBuilder builder, Throwable thrown) {
138+
public static void serializeException(StringBuilder builder, Throwable thrown, boolean stackTraceAsArray) {
142139
if (thrown != null) {
143140
builder.append("\"error.code\":\"");
144141
JsonUtils.quoteAsString(thrown.getClass().getName(), builder);
145142
builder.append("\",");
146143
builder.append("\"error.message\":\"");
147-
JsonUtils.quoteAsString(formatThrowable(thrown), builder);
144+
JsonUtils.quoteAsString(thrown.getMessage(), builder);
148145
builder.append("\",");
146+
if (stackTraceAsArray) {
147+
builder.append("\"error.stack_trace\":[").append(NEW_LINE);
148+
formatThrowableAsArray(builder, thrown);
149+
builder.append("]");
150+
} else {
151+
builder.append("\"error.stack_trace\":\"");
152+
JsonUtils.quoteAsString(formatThrowable(thrown), builder);
153+
builder.append("\"");
154+
}
149155
}
150156
}
151157

158+
public static void serializeException(StringBuilder builder, String exceptionClassName, String exceptionMessage, String stackTrace, boolean stackTraceAsArray) {
159+
builder.append("\"error.code\":\"");
160+
JsonUtils.quoteAsString(exceptionClassName, builder);
161+
builder.append("\",");
162+
builder.append("\"error.message\":\"");
163+
JsonUtils.quoteAsString(exceptionMessage, builder);
164+
builder.append("\",");
165+
if (stackTraceAsArray) {
166+
builder.append("\"error.stack_trace\":[").append(NEW_LINE);
167+
for (String line : stackTrace.split("\\n")) {
168+
appendQuoted(builder, line);
169+
}
170+
builder.append("]");
171+
} else {
172+
builder.append("\"error.stack_trace\":\"");
173+
JsonUtils.quoteAsString(stackTrace, builder);
174+
builder.append("\"");
175+
}
176+
}
177+
178+
private static void appendQuoted(StringBuilder builder, CharSequence content) {
179+
builder.append('"');
180+
JsonUtils.quoteAsString(content, builder);
181+
builder.append('"');
182+
}
183+
152184
private static CharSequence formatThrowable(final Throwable throwable) {
153-
StringWriter sw = new StringWriter(2048);
154-
final PrintWriter pw = new PrintWriter(sw);
185+
StringBuilder buffer = getMessageStringBuilder();
186+
final PrintWriter pw = new PrintWriter(new StringBuilderWriter(buffer));
155187
throwable.printStackTrace(pw);
156188
pw.flush();
157-
return sw.toString();
189+
return buffer;
190+
}
191+
192+
private static void formatThrowableAsArray(final StringBuilder jsonBuilder, final Throwable throwable) {
193+
final StringBuilder buffer = getMessageStringBuilder();
194+
final PrintWriter pw = new PrintWriter(new StringBuilderWriter(buffer), true) {
195+
@Override
196+
public void println() {
197+
flush();
198+
jsonBuilder.append("\t\"");
199+
JsonUtils.quoteAsString(buffer, jsonBuilder);
200+
jsonBuilder.append("\",");
201+
jsonBuilder.append(NEW_LINE);
202+
buffer.setLength(0);
203+
}
204+
};
205+
throwable.printStackTrace(pw);
206+
removeIfEndsWith(jsonBuilder, NEW_LINE);
207+
removeIfEndsWith(jsonBuilder, ",");
208+
}
209+
210+
public static void removeIfEndsWith(StringBuilder sb, String ending) {
211+
if (endsWith(sb, ending)) {
212+
sb.setLength(sb.length() - ending.length());
213+
}
214+
}
215+
216+
public static boolean endsWith(StringBuilder sb, String ending) {
217+
int endingLength = ending.length();
218+
int startIndex = sb.length() - endingLength;
219+
if (startIndex < 0) {
220+
return false;
221+
}
222+
for (int i = 0; i < endingLength; i++) {
223+
if (sb.charAt(startIndex + i) != ending.charAt(i)) {
224+
return false;
225+
}
226+
}
227+
return true;
228+
}
229+
230+
public static StringBuilder getMessageStringBuilder() {
231+
StringBuilder result = messageStringBuilder.get();
232+
if (result == null) {
233+
result = new StringBuilder(1024);
234+
messageStringBuilder.set(result);
235+
}
236+
result.setLength(0);
237+
return result;
238+
}
239+
240+
private static class StringBuilderWriter extends Writer {
241+
242+
private final StringBuilder buffer;
243+
244+
StringBuilderWriter(StringBuilder buffer) {
245+
this.buffer = buffer;
246+
}
247+
248+
@Override
249+
public Writer append(CharSequence csq) {
250+
buffer.append(csq);
251+
return this;
252+
}
253+
254+
@Override
255+
public void write(String str) {
256+
buffer.append(str);
257+
}
258+
259+
@Override
260+
public void write(String str, int off, int len) {
261+
buffer.append(str, off, len);
262+
}
263+
264+
@Override
265+
public Writer append(CharSequence csq, int start, int end) {
266+
buffer.append(csq, start, end);
267+
return this;
268+
}
269+
270+
@Override
271+
public Writer append(char c) {
272+
buffer.append(c);
273+
return this;
274+
}
275+
276+
@Override
277+
public void write(int c) {
278+
buffer.append((char) c);
279+
}
280+
281+
@Override
282+
public void write(char[] cbuf, int off, int len) {
283+
buffer.append(cbuf, off, len);
284+
}
285+
286+
@Override
287+
public void flush() {
288+
289+
}
290+
291+
@Override
292+
public void close() {
293+
294+
}
158295
}
159296
}

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,13 @@
2727

2828
import com.fasterxml.jackson.databind.JsonNode;
2929
import com.fasterxml.jackson.databind.ObjectMapper;
30-
import org.assertj.core.data.TemporalOffset;
31-
import org.junit.jupiter.api.Disabled;
3230
import org.junit.jupiter.api.Test;
3331

3432
import java.io.IOException;
35-
import java.io.PrintWriter;
36-
import java.io.StringWriter;
37-
import java.time.Duration;
3833
import java.time.Instant;
3934
import java.time.temporal.ChronoUnit;
35+
import java.util.stream.Collectors;
36+
import java.util.stream.StreamSupport;
4037

4138
import static org.assertj.core.api.Assertions.assertThat;
4239
import static org.assertj.core.api.Assertions.within;
@@ -88,7 +85,12 @@ void testTopLevelLabels() throws Exception {
8885
void testLogException() throws Exception {
8986
error("test", new RuntimeException("test"));
9087
assertThat(getLastLogLine().get("log.level").textValue()).isEqualTo("ERROR");
91-
assertThat(getLastLogLine().get("message").textValue()).contains("at co.elastic.logging.AbstractEcsLoggingTest.testLogException");
88+
assertThat(getLastLogLine().get("error.message").textValue()).isEqualTo("test");
89+
assertThat(getLastLogLine().get("error.code").textValue()).isEqualTo(RuntimeException.class.getName());
90+
String stackTrace = StreamSupport.stream(getLastLogLine().get("error.stack_trace").spliterator(), false)
91+
.map(JsonNode::textValue)
92+
.collect(Collectors.joining("\n", "", "\n"));
93+
assertThat(stackTrace).contains("at co.elastic.logging.AbstractEcsLoggingTest.testLogException");
9294
}
9395

9496
public abstract void putMdc(String key, String value);

0 commit comments

Comments
 (0)