After reading the log4j2 documentation many times and digging through the Apache log4j2 codebase much longer than I should have, I finally got a solution to work for me pretty cleanly.
Basically, we can use the @Plugin annotations that log4j2 provides to define our own EventResolverFactory
that'll format the stacktrace how we want it to look like. We need to define these 3 classes:
FilteredStacktraceExceptionResolverFactory.java
/**
* Defines a custom resolver to remove stacktrace lines that are irrelevant.
*
* @author Copied from Apache log4j2 and modified by takanuva15
*/
@Plugin(name = "FilteredStacktraceExceptionResolverFactory", category = TemplateResolverFactory.CATEGORY)
public final class FilteredStacktraceExceptionResolverFactory implements EventResolverFactory {
private static final FilteredStacktraceExceptionResolverFactory INSTANCE = new FilteredStacktraceExceptionResolverFactory();
private FilteredStacktraceExceptionResolverFactory() {}
@PluginFactory
public static FilteredStacktraceExceptionResolverFactory getInstance() {
return INSTANCE;
}
@Override
public String getName() {
return "filteredStacktraceException";
}
@Override
public TemplateResolver<LogEvent> create(EventResolverContext context, TemplateResolverConfig config) {
return new FilteredStacktraceExceptionResolver(context, config);
}
}
FilteredStacktraceExceptionResolver.java
/**
* Defines a custom resolver to remove stacktrace lines that are irrelevant.
*
* @author Copied from Apache log4j2 and modified by takanuva15
*/
class FilteredStacktraceExceptionResolver implements EventResolver {
private final TemplateResolver<Throwable> internalResolver;
FilteredStacktraceExceptionResolver(EventResolverContext context, TemplateResolverConfig config) {
var packagesToFilter = config.getList("packagesToFilter", String.class);
this.internalResolver = new FilteredStacktraceStackTraceStringResolver(context, packagesToFilter);
}
@Override
public void resolve(LogEvent logEvent, JsonWriter jsonWriter) {
final Throwable exception = logEvent.getThrown();
if (exception == null) {
jsonWriter.writeNull();
} else {
internalResolver.resolve(exception, jsonWriter);
}
}
@Override
public boolean isResolvable(final LogEvent logEvent) {
return logEvent.getThrown() != null;
}
}
FilteredStacktraceStringResolver.java
/**
* Defines logic to remove stacktrace lines that are irrelevant.
*
* @author Copied from Apache log4j2 and modified by takanuva15
*/
class FilteredStacktraceStackTraceStringResolver implements TemplateResolver<Throwable> {
private final Recycler<TruncatingBufferedPrintWriter> srcWriterRecycler;
private final Recycler<TruncatingBufferedPrintWriter> destWriterRecycler;
private final List<String> packagesToFilter;
FilteredStacktraceStackTraceStringResolver(EventResolverContext context, List<String> packagesToFilter) {
final Supplier<TruncatingBufferedPrintWriter> writerSupplier = () -> TruncatingBufferedPrintWriter.ofCapacity(context.getMaxStringByteCount());
final RecyclerFactory recyclerFactory = context.getRecyclerFactory();
this.srcWriterRecycler = recyclerFactory.create(writerSupplier, TruncatingBufferedPrintWriter::close);
this.destWriterRecycler = recyclerFactory.create(writerSupplier, TruncatingBufferedPrintWriter::close);
this.packagesToFilter = packagesToFilter;
}
@Override
public void resolve(Throwable throwable, JsonWriter jsonWriter) {
final TruncatingBufferedPrintWriter srcWriter = srcWriterRecycler.acquire();
try {
throwable.printStackTrace(srcWriter);
final TruncatingBufferedPrintWriter dstWriter = getFilteredStacktraceFrom(srcWriter);
jsonWriter.writeString(dstWriter);
} finally {
srcWriterRecycler.release(srcWriter);
}
}
private TruncatingBufferedPrintWriter getFilteredStacktraceFrom(final TruncatingBufferedPrintWriter srcWriter) {
final TruncatingBufferedPrintWriter destWriter = destWriterRecycler.acquire();
try {
int lineEndIndex = findNextLineStartIndex(srcWriter, 0, srcWriter.length());
destWriter.append(srcWriter, 0, lineEndIndex - 1); // write 1st line immediately to avoid extra beginning \n later
int currIndex = lineEndIndex;
int numFilteredLines = 0;
while (true) {
lineEndIndex = findNextLineStartIndex(srcWriter, currIndex, srcWriter.length());
if (lineEndIndex == -1) {
destWriter.append(System.lineSeparator());
break;
}
var doesLineStartWPkgToFilter = doesLineStartWithAPkgToFilter(srcWriter, currIndex, lineEndIndex);
if (!doesLineStartWPkgToFilter) {
if (numFilteredLines > 0) { // must write "suppressed" count before new log line
destWriter.append(String.format(" [suppressed %d lines]", numFilteredLines));
numFilteredLines = 0;
}
destWriter.append(System.lineSeparator());
destWriter.append(srcWriter, currIndex, lineEndIndex - 1);
} else {
numFilteredLines += 1;
}
currIndex = lineEndIndex;
}
} finally {
destWriterRecycler.release(destWriter);
}
return destWriter;
}
private boolean doesLineStartWithAPkgToFilter(CharSequence buffer, int currIndex, int lineEndIndex) {
if (currIndex + 4 >= lineEndIndex || !(
buffer.charAt(currIndex) == '\t' &&
buffer.charAt(currIndex + 1) == 'a' &&
buffer.charAt(currIndex + 2) == 't' &&
buffer.charAt(currIndex + 3) == ' '
)) {
return false;
}
currIndex += 4;
for (var pkg : packagesToFilter) { // a Trie of the packages would be more efficient for searching
if (currIndex + pkg.length() > lineEndIndex) {
continue; // fast-circuit to next pkg if the pkg's length is longer than remaining buffer
}
var matchFound = true;
for (int i = 0; i < pkg.length(); i++) {
if (buffer.charAt(currIndex + i) != pkg.charAt(i)) {
matchFound = false;
break;
}
}
if (matchFound) {
return true;
}
}
return false;
}
private static int findNextLineStartIndex(final CharSequence buffer, final int startIndex, final int endIndex) {
char prevChar = '-';
for (int i = startIndex; i <= endIndex; i++) {
if (prevChar == '\n') {
return i;
}
prevChar = buffer.charAt(i);
}
return -1;
}
}
(Most of the above code was copy-pasted from the existing ExceptionResolver classes in Apache log4j2. )
After adding the above classes in the codebase, I was able to do this in my JsonTemplateLayout:
{
...
"exception": {
...
"stacktrace": {
"$resolver": "filteredStacktraceException",
"packagesToFilter":[
"org.",
"java.base",
"java.lang",
"javax.",
"jdk."
]
}
},
...
}
(NOTE: Keep the list and package names as short as possible for performance)
and the stacktrace logs within the exception.stacktrace
field came out like this:
java.lang.RuntimeException: my error:
at com.mycompany.controller.MyController.get(MyController.java:266)
at com.mycompany.controller.MyController$$FastClassBySpringCGLIB$$d73ec01b.invoke(<generated>) [suppressed 11 lines]
at com.mycompany.controller.MyController$$EnhancerBySpringCGLIB$$6a28a0d1.get(<generated>) [suppressed 85 lines]
Caused by: java.lang.IllegalStateException: ahh
at com.mycompany.controller.MyController.dummyMethod(MyController.java:283)
at com.mycompany.controller.MyController.get(MyController.java:264)
... 109 more
I know the output doesn't look exactly like you were asking, but you can definitely modify the getFilteredStacktraceFrom
method above to make it look like whatever you want. I wrote my output this way to match the PatternLayout stacktrace-filtering functionality for my use case.