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

Re-implement decorate stacktrace #42200

Merged
Merged
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,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 {
phillip-kruger marked this conversation as resolved.
Show resolved Hide resolved
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,67 @@ 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 && throwable != null) {
StackTraceElement[] originalStackTrace = Arrays.copyOf(throwable.getStackTrace(), throwable.getStackTrace().length);
StackTraceElement[] stackTrace = throwable.getStackTrace();
String className = "";
String type = "java"; //default
int lineNumber = 0;
if (!knowClasses.isEmpty()) {

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));
String rootFirst = escapeHtml(ExceptionUtil.rootCauseFirstStackTrace(throwable));
if (original.contains(BRSTI)) {
original = original.replace(BRSTI,
String.format(OPEN_IDE_LINK, className, type, lineNumber));
original = original.replace(ERSTI, "</div>");
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,93 @@
package io.quarkus.runtime.logging;

import java.io.BufferedReader;
import java.io.IOException;
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);
if (contextLines != null) {
String header = "Exception in " + stackTraceElement.getFileName() + ":" + stackTraceElement.getLineNumber();
return header + "\n" + String.join("\n", contextLines);
}
} catch (IOException e) {
// Could not find the source for some reason. Just return nothing then
}
}
return null;
}

private static List<String> getRelatedLinesInSource(Path filePath, int lineNumber, int contextRange) throws IOException {
if (Files.exists(filePath)) {
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;
}
return null;
}

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
Loading