Skip to content

Commit

Permalink
Re-implement decorate stacktrace
Browse files Browse the repository at this point in the history
Signed-off-by: Phillip Kruger <phillip.kruger@gmail.com>
  • Loading branch information
phillip-kruger committed Jul 30, 2024
1 parent a02d772 commit 8f4a26b
Show file tree
Hide file tree
Showing 20 changed files with 404 additions and 394 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.quarkus.deployment.logging;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.CompositeIndex;

import io.quarkus.builder.item.SimpleBuildItem;

/**
* Contains information to decorate the Log output. Can be used by extensions that output the log / stacktraces,
* for example the error page.
*
* Also see io.quarkus.runtime.logging.DecorateStackUtil to assist with the decoration
*/
public final class LoggingDecorateBuildItem extends SimpleBuildItem {
private final Path srcMainJava;
private final CompositeIndex knowClassesIndex;

public LoggingDecorateBuildItem(Path srcMainJava, CompositeIndex knowClassesIndex) {
this.srcMainJava = srcMainJava;
this.knowClassesIndex = knowClassesIndex;
}

public Path getSrcMainJava() {
return srcMainJava;
}

public CompositeIndex getKnowClassesIndex() {
return knowClassesIndex;
}

public List<String> getKnowClasses() {
List<String> knowClasses = new ArrayList<>();
Collection<ClassInfo> knownClasses = knowClassesIndex.getKnownClasses();
for (ClassInfo ci : knownClasses) {
knowClasses.add(ci.name().toString());
}
return knowClasses;
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.quarkus.deployment.logging;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
Expand Down Expand Up @@ -45,6 +47,8 @@

import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
import io.quarkus.bootstrap.logging.InitialConfigurator;
import io.quarkus.bootstrap.model.ApplicationModel;
import io.quarkus.bootstrap.workspace.WorkspaceModule;
import io.quarkus.deployment.ApplicationArchive;
import io.quarkus.deployment.GeneratedClassGizmoAdaptor;
import io.quarkus.deployment.IsNormal;
Expand Down Expand Up @@ -86,6 +90,8 @@
import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem;
import io.quarkus.deployment.metrics.MetricsFactoryConsumerBuildItem;
import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem;
import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
import io.quarkus.deployment.recording.RecorderContext;
import io.quarkus.deployment.util.JandexUtil;
Expand All @@ -102,11 +108,13 @@
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.logging.LoggingFilter;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.configuration.ConfigInstantiator;
import io.quarkus.runtime.console.ConsoleRuntimeConfig;
import io.quarkus.runtime.logging.CategoryBuildTimeConfig;
import io.quarkus.runtime.logging.CleanupFilterConfig;
import io.quarkus.runtime.logging.DecorateStackUtil;
import io.quarkus.runtime.logging.DiscoveredLogComponents;
import io.quarkus.runtime.logging.InheritableLevel;
import io.quarkus.runtime.logging.LogBuildTimeConfig;
Expand Down Expand Up @@ -370,14 +378,25 @@ private DiscoveredLogComponents discoverLogComponents(IndexView index) {
void setupStackTraceFormatter(ApplicationArchivesBuildItem item, EffectiveIdeBuildItem ideSupport,
BuildSystemTargetBuildItem buildSystemTargetBuildItem,
List<ExceptionNotificationBuildItem> exceptionNotificationBuildItems,
CuratedApplicationShutdownBuildItem curatedApplicationShutdownBuildItem) {
CuratedApplicationShutdownBuildItem curatedApplicationShutdownBuildItem,
CurateOutcomeBuildItem curateOutcomeBuildItem,
OutputTargetBuildItem outputTargetBuildItem,
LaunchModeBuildItem launchMode,
LogBuildTimeConfig logBuildTimeConfig,
BuildProducer<LoggingDecorateBuildItem> loggingDecorateProducer) {
List<IndexView> indexList = new ArrayList<>();
for (ApplicationArchive i : item.getAllApplicationArchives()) {
if (i.getResolvedPaths().isSinglePath() && Files.isDirectory(i.getResolvedPaths().getSinglePath())) {
indexList.add(i.getIndex());
}
}
Path srcMainJava = getSourceRoot(curateOutcomeBuildItem.getApplicationModel(),
outputTargetBuildItem.getOutputDirectory());

CompositeIndex index = CompositeIndex.create(indexList);

loggingDecorateProducer.produce(new LoggingDecorateBuildItem(srcMainJava, index));

//awesome/horrible hack
//we know from the index which classes are part of the current application
//we add ANSI codes for bold and underline to their names to display them more prominently
Expand All @@ -393,6 +412,15 @@ public void accept(LogRecord logRecord, Consumer<LogRecord> logRecordConsumer) {
var elem = stackTrace[i];
if (index.getClassByName(DotName.createSimple(elem.getClassName())) != null) {
lastUserCode = stackTrace[i];

if (launchMode.getLaunchMode().equals(LaunchMode.DEVELOPMENT)
&& logBuildTimeConfig.decorateStacktraces) {
String decoratedString = DecorateStackUtil.getDecoratedString(srcMainJava, elem);
if (decoratedString != null) {
logRecord.setMessage(logRecord.getMessage() + "\n\n" + decoratedString + "\n\n");
}
}

stackTrace[i] = new StackTraceElement(elem.getClassLoaderName(), elem.getModuleName(),
elem.getModuleVersion(),
MessageFormat.UNDERLINE + MessageFormat.BOLD + elem.getClassName()
Expand Down Expand Up @@ -665,6 +693,24 @@ ConsoleCommandBuildItem logConsoleCommand() {
return new ConsoleCommandBuildItem(new LogCommand());
}

private Path getSourceRoot(ApplicationModel applicationModel, Path target) {
WorkspaceModule workspaceModule = applicationModel.getAppArtifact().getWorkspaceModule();
if (workspaceModule != null) {
return workspaceModule.getModuleDir().toPath().resolve(SRC_MAIN_JAVA);
}

if (target != null) {
var baseDir = target.getParent();
if (baseDir == null) {
baseDir = target;
}
return baseDir.resolve(SRC_MAIN_JAVA);
}
return Paths.get(SRC_MAIN_JAVA);
}

private static final String SRC_MAIN_JAVA = "src/main/java";

@GroupCommandDefinition(name = "log", description = "Logging Commands")
public static class LogCommand implements GroupCommand {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;

import io.quarkus.dev.config.CurrentConfig;
import io.quarkus.runtime.logging.DecorateStackUtil;
import io.quarkus.runtime.util.ExceptionUtil;

public class TemplateHtmlBuilder {
Expand Down Expand Up @@ -146,6 +148,11 @@ public class TemplateHtmlBuilder {

private static final String STACKTRACE_DISPLAY_DIV = "<div id=\"stacktrace\"></div>";

private static final String BRSTI = "___begin_relative_stack_trace_item___";
private static final String ERSTI = "___end_relative_stack_trace_item___";

private static final String OPEN_IDE_LINK = "<div class='rel-stacktrace-item' onclick=\"event.preventDefault(); fetch('/q/open-in-ide/%s/%s/%d');\">";

private static final String ERROR_STACK = " <div id=\"original-stacktrace\" class=\"trace hidden\">\n" +
"<h3>The stacktrace below is the original. " +
"<a href=\"\" onClick=\"toggleStackTraceOrder(); return false;\">See the stacktrace in reversed order</a> (root-cause first)</h4>"
Expand All @@ -159,6 +166,7 @@ public class TemplateHtmlBuilder {
" <code class=\"stacktrace\"><pre>%1$s</pre></code>\n" +
" </div>\n";

private static final String DECORATE_DIV = "<pre class='decorate'>%s</pre>";
private static final String CONFIG_EDITOR_HEAD = "<h3>The following incorrect config values were detected:</h3>" +
"<form class=\"updateConfigForm\" method=\"post\" enctype=\"application/x-www-form-urlencoded\" action=\"/io.quarkus.vertx-http.devmode.config.fix\">"
+ "<input type=\"hidden\" name=\"redirect\" value=\"%s\"/>\n";
Expand Down Expand Up @@ -218,10 +226,62 @@ public TemplateHtmlBuilder(String baseUrl, String title, String subTitle, String
}
}

public TemplateHtmlBuilder decorate(final Throwable throwable, String srcMainJava, List<String> knowClasses) {
String decoratedString = DecorateStackUtil.getDecoratedString(throwable, srcMainJava, knowClasses);
if (decoratedString != null) {
result.append(String.format(DECORATE_DIV, decoratedString));
}

return this;
}

public TemplateHtmlBuilder stack(final Throwable throwable) {
result.append(String.format(ERROR_STACK, escapeHtml(ExceptionUtil.generateStackTrace(throwable))));
result.append(String.format(ERROR_STACK_REVERSED, escapeHtml(ExceptionUtil.rootCauseFirstStackTrace(throwable))));
result.append(STACKTRACE_DISPLAY_DIV);
return stack(throwable, List.of());
}

public TemplateHtmlBuilder stack(final Throwable throwable, List<String> knowClasses) {
if (knowClasses != null && !knowClasses.isEmpty() && throwable != null) {
StackTraceElement[] originalStackTrace = Arrays.copyOf(throwable.getStackTrace(), throwable.getStackTrace().length);
StackTraceElement[] stackTrace = throwable.getStackTrace();
String className = "";
String type = "java"; //default
int lineNumber = 0;
for (int i = 0; i < stackTrace.length; ++i) {
var elem = stackTrace[i];

className = elem.getClassName();
String filename = elem.getFileName();
if (filename != null) {
int dotindex = filename.lastIndexOf(".");
type = elem.getFileName().substring(dotindex + 1);
}
lineNumber = elem.getLineNumber();

if (knowClasses.contains(elem.getClassName())) {
stackTrace[i] = new StackTraceElement(elem.getClassLoaderName(), elem.getModuleName(),
elem.getModuleVersion(),
BRSTI + elem.getClassName()
+ ERSTI,
elem.getMethodName(), elem.getFileName(), elem.getLineNumber());
}
}
throwable.setStackTrace(stackTrace);

String original = escapeHtml(ExceptionUtil.generateStackTrace(throwable));
original = original.replace(BRSTI,
String.format(OPEN_IDE_LINK, className, type, lineNumber));
original = original.replace(ERSTI, "</div>");
String rootFirst = escapeHtml(ExceptionUtil.rootCauseFirstStackTrace(throwable));
rootFirst = rootFirst.replace(BRSTI,
String.format(OPEN_IDE_LINK, className, type, lineNumber));
rootFirst = rootFirst.replace(ERSTI, "</div>");

result.append(String.format(ERROR_STACK, original));
result.append(String.format(ERROR_STACK_REVERSED, rootFirst));
result.append(STACKTRACE_DISPLAY_DIV);

throwable.setStackTrace(originalStackTrace);
}
return this;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.quarkus.runtime.logging;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

public class DecorateStackUtil {

public static String getDecoratedString(final Throwable throwable, String srcMainJava, List<String> knowClasses) {
if (srcMainJava != null) {
return DecorateStackUtil.getDecoratedString(throwable, Path.of(srcMainJava), knowClasses);
}
return null;
}

public static String getDecoratedString(final Throwable throwable, Path srcMainJava, List<String> knowClasses) {
if (knowClasses != null && !knowClasses.isEmpty() && throwable != null) {
StackTraceElement[] stackTrace = throwable.getStackTrace();
for (int i = 0; i < stackTrace.length; ++i) {
StackTraceElement elem = stackTrace[i];
if (knowClasses.contains(elem.getClassName())) {
String decoratedString = DecorateStackUtil.getDecoratedString(srcMainJava, elem);
if (decoratedString != null) {
return decoratedString;
}
}
}
}

return null;
}

public static String getDecoratedString(Path srcMainJava, StackTraceElement stackTraceElement) {
int lineNumber = stackTraceElement.getLineNumber();
if (lineNumber > 0 && srcMainJava != null) {
String fullJavaFileName = getFullPath(stackTraceElement.getClassName(), stackTraceElement.getFileName());
Path f = srcMainJava.resolve(fullJavaFileName);
try {
List<String> contextLines = DecorateStackUtil.getRelatedLinesInSource(f, lineNumber, 2);
String header = "Exception in " + stackTraceElement.getFileName() + ":" + stackTraceElement.getLineNumber();
return header + "\n" + String.join("\n", contextLines);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
return null;
}

private static List<String> getRelatedLinesInSource(Path filePath, int lineNumber, int contextRange) throws IOException {
List<String> resultLines = new ArrayList<>();
Deque<String> contextQueue = new ArrayDeque<>(2 * contextRange + 1);
try (BufferedReader reader = Files.newBufferedReader(filePath)) {
String line;
int currentLine = 1;
while ((line = reader.readLine()) != null) {
if (currentLine >= lineNumber - contextRange) {
String ln = String.valueOf(currentLine);
if (currentLine == lineNumber) {
ln = "-> " + ln + " ";
} else {
ln = " " + ln + " ";
}

contextQueue.add("\t" + ln + line);
}
if (currentLine >= lineNumber + contextRange) {
break;
}
currentLine++;
}
resultLines.addAll(contextQueue);
}
return resultLines;
}

private static String getFullPath(String fullClassName, String fileName) {
int lastDotIndex = fullClassName.lastIndexOf(".");
String packageName = fullClassName.substring(0, lastDotIndex);
String path = packageName.replace('.', '/');
return path + "/" + fileName;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ public class LogBuildTimeConfig {
@ConfigItem(defaultValue = "DEBUG")
public Level minLevel;

/**
* This will decorate the stacktrace in dev mode to show the line in the code that cause the exception
*/
@ConfigItem(defaultValue = "true")
public Boolean decorateStacktraces;

/**
* Minimum logging categories.
* <p>
Expand Down
17 changes: 17 additions & 0 deletions core/runtime/src/main/resources/META-INF/template-html-builder.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 8f4a26b

Please sign in to comment.