Is there a way to override the ExceptionResolver of JsonTemplateLayout of Log4j2
Asked Answered
D

1

3

With SpringBoot + OpenTelemetry the stack-trace for every exception runs into 100s of lines. Most of the lines apart from the top few are often not useful in troubleshooting the issue. The default truncation can solve the large event problem, but as it could be arbitrary we may miss some key data.

Our requirement is to produce something of this sort in the JSONTemplateLayout context:

java.lang.Exception: top-level-exception
  at MyClass.methodA(MyClass.java:25)
  <15 more frames, maximum>
  ...truncated... 
Caused by: java.lang.RuntimeException: root-cause-exception
 at SomeOtherClass.methodB(SomeOtherClass.java:55)
  <15 more frames, maximum>
  ...truncated... 

This way we don't lose the chain that caused the exception to truncation but at the same time not have more than N frames at each level in the chain (N=16 in the example above).

If there's a way to pass in the resolver for stack trace this could work, but I couldn't find a way to pass in a custom resolver.

Diphenylamine answered 6/1, 2022 at 22:42 Comment(1)
hey were you ever able to figure this out?Trifle
T
0

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.

Trifle answered 20/9, 2023 at 13:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.