Skip to content

Commit

Permalink
Pluggable stack trace formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
dmlloyd committed Nov 16, 2023
1 parent bd6c0cd commit ffac943
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 165 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2017 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed 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.
*/

package org.jboss.logmanager.formatters;

import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;

/**
* Formatter used to format the stack trace of an exception.
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
final class BasicStackTraceFormatter {
private static final String CAUSED_BY_CAPTION = "Caused by: ";
private static final String SUPPRESSED_CAPTION = "Suppressed: ";

private final Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
private final StringBuilder builder;
private final int suppressedDepth;
private int suppressedCount;

private BasicStackTraceFormatter(final StringBuilder builder, final int suppressedDepth) {
this.builder = builder;
this.suppressedDepth = suppressedDepth;
}

/**
* Writes the stack trace into the builder.
*
* @param builder the string builder ot append the stack trace to
* @param t the throwable to render
* @param suppressedDepth the number of suppressed messages to include
*/
public static void renderStackTrace(final StringBuilder builder, final Throwable t, final int suppressedDepth) {
renderStackTrace(builder, t, false, suppressedDepth);
}

/**
* Writes the stack trace into the builder.
*
* @param builder the string builder ot append the stack trace to
* @param t the throwable to render
* @param extended ignored
* @param suppressedDepth the number of suppressed messages to include
*/
static void renderStackTrace(final StringBuilder builder, final Throwable t,
@SuppressWarnings("unused") final boolean extended, final int suppressedDepth) {
new BasicStackTraceFormatter(builder, suppressedDepth).renderStackTrace(t);
}

private void renderStackTrace(final Throwable t) {
// Reset the suppression count
suppressedCount = 0;
// Write the exception message
builder.append(": ").append(t);
newLine();

// Write the stack trace for this message
final StackTraceElement[] stackTrace = t.getStackTrace();
for (StackTraceElement element : stackTrace) {
renderTrivial("", element);
}

// Write any suppressed messages, if required
if (suppressedDepth != 0) {
for (Throwable se : t.getSuppressed()) {
if (suppressedDepth < 0 || suppressedDepth > suppressedCount++) {
renderStackTrace(stackTrace, se, SUPPRESSED_CAPTION, "\t");
}
}
}

// Print cause if there is one
final Throwable ourCause = t.getCause();
if (ourCause != null) {
renderStackTrace(stackTrace, ourCause, CAUSED_BY_CAPTION, "");
}
}

private void renderStackTrace(final StackTraceElement[] parentStack, final Throwable child, final String caption,
final String prefix) {
if (seen.contains(child)) {
builder.append(prefix)
.append(caption)
.append("[CIRCULAR REFERENCE: ")
.append(child)
.append(']');
newLine();
} else {
seen.add(child);
// Find the unique frames suppressing duplicates
final StackTraceElement[] causeStack = child.getStackTrace();
int m = causeStack.length - 1;
int n = parentStack.length - 1;
while (m >= 0 && n >= 0 && causeStack[m].equals(parentStack[n])) {
m--;
n--;
}
final int framesInCommon = causeStack.length - 1 - m;

// Print our stack trace
builder.append(prefix)
.append(caption)
.append(child);
newLine();
for (int i = 0; i <= m; i++) {
renderTrivial(prefix, causeStack[i]);
}
if (framesInCommon != 0) {
builder.append(prefix)
.append("\t... ")
.append(framesInCommon)
.append(" more");
newLine();
}

// Print suppressed exceptions, if any
if (suppressedDepth != 0) {
for (Throwable se : child.getSuppressed()) {
if (suppressedDepth < 0 || suppressedDepth > suppressedCount++) {
renderStackTrace(causeStack, se, SUPPRESSED_CAPTION, prefix + "\t");
}
}
}

// Print cause, if any
Throwable ourCause = child.getCause();
if (ourCause != null) {
renderStackTrace(causeStack, ourCause, CAUSED_BY_CAPTION, prefix);
}
}
}

private void renderTrivial(final String prefix, final StackTraceElement element) {
builder.append(prefix)
.append("\tat ")
.append(element);
newLine();
}

private void newLine() {
builder.append(System.lineSeparator());
}
}
57 changes: 44 additions & 13 deletions src/main/java/org/jboss/logmanager/formatters/Formatters.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import static java.lang.Math.min;
import static java.security.AccessController.doPrivileged;

import java.io.IOException;
import java.io.PrintWriter;
import java.security.PrivilegedAction;
import java.time.Duration;
Expand Down Expand Up @@ -884,7 +885,8 @@ public ItemType getItemType() {
*/
public static FormatStep exceptionFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth,
final boolean extended) {
return exceptionFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth, null, extended);
return exceptionFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth, null, extended,
StackTraceFormatter.instance());
}

/**
Expand All @@ -895,42 +897,71 @@ public static FormatStep exceptionFormatStep(final boolean leftJustify, final in
* @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end
* @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none
* @param extended {@code true} if the stack trace should attempt to include extended JAR version information
* @param formatter the stack trace formatter to use (must not be {@code null})
* @return the format step
*/
public static FormatStep exceptionFormatStep(final boolean leftJustify, final int minimumWidth,
final boolean truncateBeginning, final int maximumWidth, final String argument, final boolean extended) {
final boolean truncateBeginning, final int maximumWidth, final String argument, final boolean extended,
final StackTraceFormatter formatter) {
int depth = -1;
try {
depth = Integer.parseInt(argument);
} catch (NumberFormatException ignored) {
}
final int finalDepth = depth;
StackTraceFormatter.Parameters params = new StackTraceFormatter.Parameters() {
public boolean extended() {
return extended;
}

public int suppressedDepth() {
return finalDepth;
}
};
return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) {
// not really correct but doesn't matter for now
public ItemType getItemType() {
return ItemType.EXCEPTION_TRACE;
}

public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) {
public void renderRaw(Formatter fmt, final StringBuilder builder, final ExtLogRecord record) {
if (System.getSecurityManager() != null)
doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
doExceptionFormatStep(builder, record, argument, extended);
doExceptionFormatStep(builder, record, argument, extended, formatter, params);
return null;
}
});
else
doExceptionFormatStep(builder, record, argument, extended);
doExceptionFormatStep(builder, record, argument, extended, formatter, params);
}
};
}

/**
* Create a format step which emits the stack trace of an exception with the given justification rules.
*
* @param leftJustify {@code true} to left justify, {@code false} to right justify
* @param minimumWidth the minimum field width, or 0 for none
* @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end
* @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none
* @param extended {@code true} if the stack trace should attempt to include extended JAR version information
* @return the format step
*/
public static FormatStep exceptionFormatStep(final boolean leftJustify, final int minimumWidth,
final boolean truncateBeginning, final int maximumWidth, final String argument, final boolean extended) {
return exceptionFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, argument, extended,
StackTraceFormatter.instance());
}

private static void doExceptionFormatStep(final StringBuilder builder, final ExtLogRecord record, final String argument,
final boolean extended) {
final boolean extended, final StackTraceFormatter formatter, final StackTraceFormatter.Parameters params) {
final Throwable t = record.getThrown();
if (t != null) {
int depth = -1;
if (argument != null) {
try {
depth = Integer.parseInt(argument);
} catch (NumberFormatException ignore) {
}
try {
formatter.render(t, builder, params);
} catch (IOException ignored) {
}
StackTraceFormatter.renderStackTrace(builder, t, extended, depth);
}
}

Expand Down
Loading

0 comments on commit ffac943

Please sign in to comment.