Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pluggable stack trace formatting #447

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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