diff --git a/docs/tab-widgets/ecs-encoder.asciidoc b/docs/tab-widgets/ecs-encoder.asciidoc index 8d538cfb..9792794b 100644 --- a/docs/tab-widgets/ecs-encoder.asciidoc +++ b/docs/tab-widgets/ecs-encoder.asciidoc @@ -176,6 +176,15 @@ To include any custom field in the output, use following syntax: Custom fields are included in the order they are declared. The values support https://logging.apache.org/log4j/2.x/manual/lookups.html[lookups]. +To suppress lines containing `com.example.SomeClass` in an exceptions stack trace, use the following syntax: + +[source,xml] +---- + + + +---- + NOTE: The log4j2 `EcsLayout` does not allocate any memory (unless the log event contains an `Exception`) to reduce GC pressure. This is achieved by manually serializing JSON so that no intermediate JSON or map representation of a log event is needed. // end::log4j2[] diff --git a/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java b/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java index 3dbb5cf4..33b4fb4e 100644 --- a/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java +++ b/ecs-logging-core/src/main/java/co/elastic/logging/EcsJsonSerializer.java @@ -26,6 +26,7 @@ import java.io.PrintWriter; import java.io.Writer; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -188,6 +189,10 @@ public static void serializeMDC(StringBuilder builder, Map properties } public static void serializeException(StringBuilder builder, Throwable thrown, boolean stackTraceAsArray) { + serializeException(builder, thrown, Collections.emptyList(), stackTraceAsArray); + } + + public static void serializeException(StringBuilder builder, Throwable thrown, List stackTraceFilters, boolean stackTraceAsArray) { if (thrown != null) { builder.append("\"error.type\":\""); JsonUtils.quoteAsString(thrown.getClass().getName(), builder); @@ -201,11 +206,11 @@ public static void serializeException(StringBuilder builder, Throwable thrown, b } if (stackTraceAsArray) { builder.append("\"error.stack_trace\":[").append(NEW_LINE); - formatThrowableAsArray(builder, thrown); + formatThrowableAsArray(builder, thrown, stackTraceFilters); builder.append("]"); } else { builder.append("\"error.stack_trace\":\""); - JsonUtils.quoteAsString(formatThrowable(thrown), builder); + JsonUtils.quoteAsString(formatThrowable(thrown, stackTraceFilters), builder); builder.append("\""); } } @@ -232,19 +237,77 @@ public static void serializeException(StringBuilder builder, String exceptionCla } } - private static CharSequence formatThrowable(final Throwable throwable) { - StringBuilder buffer = getMessageStringBuilder(); - final PrintWriter pw = new PrintWriter(new StringBuilderWriter(buffer)); + private static CharSequence formatThrowable(final Throwable throwable, final List stackTraceFilters) { + final StringBuilder buffer = getMessageStringBuilder(); + final PrintWriter pw = new PrintWriter(new StringBuilderWriter(buffer)) { + + private int endOfLastLine; + private int numberOfSuppressedLines; + + @Override + public void println() { + if (buffer.indexOf("\tat", endOfLastLine) == endOfLastLine) { + for (String filter : stackTraceFilters) { + if (buffer.indexOf(filter, endOfLastLine) != -1) { + ++numberOfSuppressedLines; + buffer.setLength(endOfLastLine); + + return; + } + } + } + if (numberOfSuppressedLines > 0) { + int lengthOfNextLine = buffer.length() - endOfLastLine; + if (numberOfSuppressedLines > 1) { + buffer.insert(endOfLastLine, "\t... suppressed ") + .insert(buffer.length() - lengthOfNextLine, numberOfSuppressedLines) + .insert(buffer.length() - lengthOfNextLine, " lines"); + } else { + buffer.insert(endOfLastLine, "\t..."); + } + buffer.insert(buffer.length() - lengthOfNextLine, NEW_LINE); + + numberOfSuppressedLines = 0; + } + buffer.append(NEW_LINE); + endOfLastLine = buffer.length(); + } + }; throwable.printStackTrace(pw); pw.flush(); return buffer; } - private static void formatThrowableAsArray(final StringBuilder jsonBuilder, final Throwable throwable) { + private static void formatThrowableAsArray(final StringBuilder jsonBuilder, final Throwable throwable, final List stackTraceFilters) { final StringBuilder buffer = getMessageStringBuilder(); final PrintWriter pw = new PrintWriter(new StringBuilderWriter(buffer), true) { + + private int numberOfSuppressedLines; + @Override public void println() { + if (buffer.indexOf("\tat") == 0) { + for (String filter : stackTraceFilters) { + if (buffer.indexOf(filter) != -1) { + ++numberOfSuppressedLines; + buffer.setLength(0); + + return; + } + } + } + if (numberOfSuppressedLines > 0) { + jsonBuilder.append("\t\""); + if (numberOfSuppressedLines > 1) { + jsonBuilder.append("\\t... suppressed ").append(numberOfSuppressedLines).append(" lines"); + } else { + jsonBuilder.append("\\t..."); + } + jsonBuilder.append("\","); + jsonBuilder.append(NEW_LINE); + + numberOfSuppressedLines = 0; + } flush(); jsonBuilder.append("\t\""); JsonUtils.quoteAsString(buffer, jsonBuilder); diff --git a/ecs-logging-core/src/test/java/co/elastic/logging/EcsJsonSerializerTest.java b/ecs-logging-core/src/test/java/co/elastic/logging/EcsJsonSerializerTest.java index 73f088d2..14e479a6 100644 --- a/ecs-logging-core/src/test/java/co/elastic/logging/EcsJsonSerializerTest.java +++ b/ecs-logging-core/src/test/java/co/elastic/logging/EcsJsonSerializerTest.java @@ -173,4 +173,94 @@ private void assertRemoveIfEndsWith(String builder, String ending, String expect EcsJsonSerializer.removeIfEndsWith(sb, ending); assertThat(sb.toString()).isEqualTo(expected); } + + @Test + void testStackTraceSuppressOneLine() throws IOException { + try { + First.m(); + } catch (Exception exception) { + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append('{'); + List stackTraceFilters = List.of("co.elastic.logging.EcsJsonSerializerTest$First"); + EcsJsonSerializer.serializeException(jsonBuilder, exception, stackTraceFilters, false); + jsonBuilder.append('}'); + + String stackTrace = objectMapper.readTree(jsonBuilder.toString()).get(ERROR_STACK_TRACE).textValue(); + assertThat(stackTrace).contains("\n\t...\n"); + assertThat(stackTrace).doesNotContain("co.elastic.logging.EcsJsonSerializerTest$First"); + } + } + + @Test + void testWihStackTraceAsArrayStackTraceSuppressOneLine() throws IOException { + try { + First.m(); + } catch (Exception exception) { + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append('{'); + List stackTraceFilters = List.of("co.elastic.logging.EcsJsonSerializerTest$First"); + EcsJsonSerializer.serializeException(jsonBuilder, exception, stackTraceFilters, true); + jsonBuilder.append('}'); + + String stackTrace = objectMapper.readTree(jsonBuilder.toString()).get(ERROR_STACK_TRACE).toString(); + assertThat(stackTrace).contains("\"\\t...\""); + assertThat(stackTrace).doesNotContain("co.elastic.logging.EcsJsonSerializerTest$First"); + } + } + + @Test + void testStackTraceSuppressMultipleLines() throws IOException { + try { + First.m(); + } catch (Exception exception) { + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append('{'); + List stackTraceFilters = List.of("co.elastic.logging.EcsJsonSerializerTest$First", "co.elastic.logging.EcsJsonSerializerTest$Second"); + EcsJsonSerializer.serializeException(jsonBuilder, exception, stackTraceFilters, false); + jsonBuilder.append('}'); + + String stackTrace = objectMapper.readTree(jsonBuilder.toString()).get(ERROR_STACK_TRACE).textValue(); + assertThat(stackTrace).contains("\n\t... suppressed 2 lines\n"); + assertThat(stackTrace).doesNotContain("co.elastic.logging.EcsJsonSerializerTest$First"); + assertThat(stackTrace).doesNotContain("co.elastic.logging.EcsJsonSerializerTest$Second"); + } + } + + @Test + void testWithStackTraceAsArrayStackTraceSuppressMultipleLines() throws IOException { + try { + First.m(); + } catch (Exception exception) { + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append('{'); + List stackTraceFilters = List.of("co.elastic.logging.EcsJsonSerializerTest$First", "co.elastic.logging.EcsJsonSerializerTest$Second"); + EcsJsonSerializer.serializeException(jsonBuilder, exception, stackTraceFilters, true); + jsonBuilder.append('}'); + + String stackTrace = objectMapper.readTree(jsonBuilder.toString()).get(ERROR_STACK_TRACE).toString(); + assertThat(stackTrace).contains("\"\\t... suppressed 2 lines\""); + assertThat(stackTrace).doesNotContain("co.elastic.logging.EcsJsonSerializerTest$First"); + assertThat(stackTrace).doesNotContain("co.elastic.logging.EcsJsonSerializerTest$Second"); + } + } + + + private static final class First { + public static void m() { + Second.m(); + } + } + + + private static final class Second { + public static void m() { + Third.m(); + } + } + + private static final class Third { + public static void m() { + throw new RuntimeException(); + } + } } diff --git a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java index 5c963d1a..775bf991 100644 --- a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java +++ b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/EcsLayout.java @@ -52,6 +52,7 @@ import org.apache.logging.log4j.util.StringBuilderFormattable; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -69,6 +70,7 @@ public class EcsLayout extends AbstractStringLayout { private final KeyValuePair[] additionalFields; private final PatternFormatter[][] fieldValuePatternFormatter; + private final List stackTraceFilter; private final boolean stackTraceAsArray; private final String serviceName; private final String serviceNodeName; @@ -78,13 +80,14 @@ public class EcsLayout extends AbstractStringLayout { private final ConcurrentMap, Boolean> supportsJson = new ConcurrentHashMap, Boolean>(); private EcsLayout(Configuration config, String serviceName, String serviceNodeName, String eventDataset, boolean includeMarkers, - KeyValuePair[] additionalFields, boolean includeOrigin, boolean stackTraceAsArray) { + KeyValuePair[] additionalFields, boolean includeOrigin, List stackTraceFilter, boolean stackTraceAsArray) { super(config, UTF_8, null, null); this.serviceName = serviceName; this.serviceNodeName = serviceNodeName; this.eventDataset = eventDataset; this.includeMarkers = includeMarkers; this.includeOrigin = includeOrigin; + this.stackTraceFilter = stackTraceFilter; this.stackTraceAsArray = stackTraceAsArray; this.additionalFields = additionalFields; fieldValuePatternFormatter = new PatternFormatter[additionalFields.length][]; @@ -140,7 +143,7 @@ private StringBuilder toText(LogEvent event, StringBuilder builder, boolean gcFr if (includeOrigin) { EcsJsonSerializer.serializeOrigin(builder, event.getSource()); } - EcsJsonSerializer.serializeException(builder, event.getThrown(), stackTraceAsArray); + EcsJsonSerializer.serializeException(builder, event.getThrown(), stackTraceFilter, stackTraceAsArray); EcsJsonSerializer.serializeObjectEnd(builder); return builder; } @@ -339,6 +342,8 @@ public static class Builder implements org.apache.logging.log4j.core.util.Builde private boolean includeMarkers = false; @PluginBuilderAttribute("stackTraceAsArray") private boolean stackTraceAsArray = false; + @PluginElement("StackTraceFilters") + private StackTraceFilter[] stackTraceFilters = new StackTraceFilter[]{}; @PluginElement("AdditionalField") private KeyValuePair[] additionalFields = new KeyValuePair[]{}; @PluginBuilderAttribute("includeOrigin") @@ -380,6 +385,14 @@ public boolean isIncludeOrigin() { return includeOrigin; } + public boolean isStackTraceAsArray() { + return stackTraceAsArray; + } + + public StackTraceFilter[] getStackTraceFilters() { + return stackTraceFilters; + } + /** * Additional fields to set on each log event. * @@ -415,6 +428,11 @@ public EcsLayout.Builder setIncludeOrigin(final boolean includeOrigin) { return this; } + public EcsLayout.Builder setStackTraceFilters(StackTraceFilter[] stackTraceFilters) { + this.stackTraceFilters = stackTraceFilters; + return this; + } + public EcsLayout.Builder setStackTraceAsArray(boolean stackTraceAsArray) { this.stackTraceAsArray = stackTraceAsArray; return this; @@ -422,12 +440,12 @@ public EcsLayout.Builder setStackTraceAsArray(boolean stackTraceAsArray) { @Override public EcsLayout build() { + List stackTraceFilterPatterns = new ArrayList(stackTraceFilters.length); + for (StackTraceFilter stackTraceFilter : stackTraceFilters) { + stackTraceFilterPatterns.add(stackTraceFilter.getFilter()); + } return new EcsLayout(getConfiguration(), serviceName, serviceNodeName, EcsJsonSerializer.computeEventDataset(eventDataset, serviceName), - includeMarkers, additionalFields, includeOrigin, stackTraceAsArray); - } - - public boolean isStackTraceAsArray() { - return stackTraceAsArray; + includeMarkers, additionalFields, includeOrigin, stackTraceFilterPatterns, stackTraceAsArray); } } } diff --git a/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/StackTraceFilter.java b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/StackTraceFilter.java new file mode 100644 index 00000000..ab1ad41e --- /dev/null +++ b/log4j2-ecs-layout/src/main/java/co/elastic/logging/log4j2/StackTraceFilter.java @@ -0,0 +1,58 @@ +/*- + * #%L + * Java ECS logging + * %% + * Copyright (C) 2019 - 2022 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + * #L% + */ +package co.elastic.logging.log4j2; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.status.StatusLogger; + +@Plugin(name = "StackTraceFilter", category = Node.CATEGORY) +public final class StackTraceFilter { + + private static final Logger LOGGER = StatusLogger.getLogger(); + + private final String filter; + + public StackTraceFilter(String filter) { + this.filter = filter; + } + + public String getFilter() { + return filter; + } + + @PluginFactory + public static StackTraceFilter createStackTraceFilter(@PluginAttribute("filter") final String filter) { + if (filter == null) { + LOGGER.error("StackTraceFilter must contain a filter"); + return null; + } + + return new StackTraceFilter(filter); + } +} diff --git a/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/StackTraceFilterTest.java b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/StackTraceFilterTest.java new file mode 100644 index 00000000..0037e50c --- /dev/null +++ b/log4j2-ecs-layout/src/test/java/co/elastic/logging/log4j2/StackTraceFilterTest.java @@ -0,0 +1,63 @@ +/*- + * #%L + * Java ECS logging + * %% + * Copyright (C) 2019 - 2022 Elastic and contributors + * %% + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + * #L% + */ +package co.elastic.logging.log4j2; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.impl.DefaultLogEventFactory; +import org.apache.logging.log4j.message.SimpleMessage; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class StackTraceFilterTest { + + @Test + void testStackTracFilter() { + EcsLayout ecsLayout = new EcsLayout.Builder() + .setStackTraceFilters(new StackTraceFilter[]{new StackTraceFilter("co.elastic.logging.log4j2.StackTraceFilterTest")}) + .build(); + String formattedEvent = ecsLayout.toSerializable(createLogEvent()); + + assertThat(formattedEvent).contains("\\n\\t... suppressed 2 lines\\n"); + assertThat(formattedEvent).doesNotContain("co.elastic.logging.log4j2.StackTraceFilterTest"); + } + + @Test + void testStackTracFilterWithStackTraceAsArray() { + EcsLayout ecsLayout = new EcsLayout.Builder() + .setStackTraceFilters(new StackTraceFilter[]{new StackTraceFilter("co.elastic.logging.log4j2.StackTraceFilterTest")}) + .setStackTraceAsArray(true) + .build(); + String formattedEvent = ecsLayout.toSerializable(createLogEvent()); + + assertThat(formattedEvent).contains("\"\\t... suppressed 2 lines\""); + assertThat(formattedEvent).doesNotContain("co.elastic.logging.log4j2.StackTraceFilterTest"); + } + + private static LogEvent createLogEvent() { + return new DefaultLogEventFactory().createEvent("loggerName", null, null, Level.INFO, new SimpleMessage("Something useful"), null, new Exception()); + } +}