diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingDecorateBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingDecorateBuildItem.java new file mode 100644 index 0000000000000..500a84f90b0f1 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingDecorateBuildItem.java @@ -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 getKnowClasses() { + List knowClasses = new ArrayList<>(); + Collection knownClasses = knowClassesIndex.getKnownClasses(); + for (ClassInfo ci : knownClasses) { + knowClasses.add(ci.name().toString()); + } + return knowClasses; + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java index 4bc1ee15c7e94..9505e778c425b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java @@ -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; @@ -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; @@ -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; @@ -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; @@ -370,14 +378,25 @@ private DiscoveredLogComponents discoverLogComponents(IndexView index) { void setupStackTraceFormatter(ApplicationArchivesBuildItem item, EffectiveIdeBuildItem ideSupport, BuildSystemTargetBuildItem buildSystemTargetBuildItem, List exceptionNotificationBuildItems, - CuratedApplicationShutdownBuildItem curatedApplicationShutdownBuildItem) { + CuratedApplicationShutdownBuildItem curatedApplicationShutdownBuildItem, + CurateOutcomeBuildItem curateOutcomeBuildItem, + OutputTargetBuildItem outputTargetBuildItem, + LaunchModeBuildItem launchMode, + LogBuildTimeConfig logBuildTimeConfig, + BuildProducer loggingDecorateProducer) { List 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 @@ -393,6 +412,15 @@ public void accept(LogRecord logRecord, Consumer 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() @@ -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 { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java index 22b3cce91e9f2..e041748577ffa 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java @@ -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 { @@ -146,6 +148,11 @@ public class TemplateHtmlBuilder { private static final String STACKTRACE_DISPLAY_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 = "
"; + private static final String ERROR_STACK = "
\n" + "

The stacktrace below is the original. " + "See the stacktrace in reversed order (root-cause first)

" @@ -159,6 +166,7 @@ public class TemplateHtmlBuilder { "
%1$s
\n" + "
\n"; + private static final String DECORATE_DIV = "
%s
"; private static final String CONFIG_EDITOR_HEAD = "

The following incorrect config values were detected:

" + "
" + "\n"; @@ -218,10 +226,67 @@ public TemplateHtmlBuilder(String baseUrl, String title, String subTitle, String } } + public TemplateHtmlBuilder decorate(final Throwable throwable, String srcMainJava, List 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 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, "
"); + rootFirst = rootFirst.replace(BRSTI, + String.format(OPEN_IDE_LINK, className, type, lineNumber)); + rootFirst = rootFirst.replace(ERSTI, ""); + } + + 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; } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/DecorateStackUtil.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/DecorateStackUtil.java new file mode 100644 index 0000000000000..8b4c79b863cdc --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/DecorateStackUtil.java @@ -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 knowClasses) { + if (srcMainJava != null) { + return DecorateStackUtil.getDecoratedString(throwable, Path.of(srcMainJava), knowClasses); + } + return null; + } + + public static String getDecoratedString(final Throwable throwable, Path srcMainJava, List 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 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 getRelatedLinesInSource(Path filePath, int lineNumber, int contextRange) throws IOException { + List resultLines = new ArrayList<>(); + Deque 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; + } + +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java index 0f9914985fccf..203a0d7f654b2 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogBuildTimeConfig.java @@ -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. *

diff --git a/core/runtime/src/main/resources/META-INF/template-html-builder.css b/core/runtime/src/main/resources/META-INF/template-html-builder.css index 7760fd6de8743..6cf8325e64a5e 100644 --- a/core/runtime/src/main/resources/META-INF/template-html-builder.css +++ b/core/runtime/src/main/resources/META-INF/template-html-builder.css @@ -114,11 +114,28 @@ h3 { display: none; } +.decorate { + font-size: 1.3em; + line-height: 1.5; +} + .stacktrace { font-size: 1.3em; line-height: 1.5; + color: hsla(3, 89%, 42%, 1.0); +} + +.rel-stacktrace-item { + font-weight: 400; + display: inline; } +.rel-stacktrace-item:hover { + text-decoration: underline; + cursor: pointer; +} + + a, a:visited, a:focus, a:active { text-decoration: none; color:hsla(211, 63%, 54%, 1.0); } diff --git a/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java b/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java index 2e14e0bf8e015..240a96eb87f83 100644 --- a/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java +++ b/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java @@ -14,6 +14,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -105,11 +106,13 @@ import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; +import io.quarkus.deployment.logging.LoggingDecorateBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.deployment.util.ServiceUtil; import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.logging.LogBuildTimeConfig; import io.quarkus.undertow.runtime.HttpSessionContext; import io.quarkus.undertow.runtime.QuarkusIdentityManager; import io.quarkus.undertow.runtime.ServletHttpSecurityPolicy; @@ -392,6 +395,8 @@ public ServletDeploymentManagerBuildItem build(List servlets, LaunchModeBuildItem launchMode, ShutdownContextBuildItem shutdownContext, KnownPathsBuildItem knownPaths, + LogBuildTimeConfig logBuildTimeConfig, + Optional loggingDecorateBuildItem, HttpBuildTimeConfig httpBuildTimeConfig, HttpRootPathBuildItem httpRootPath, ServletConfig servletConfig, @@ -682,8 +687,21 @@ public ServletDeploymentManagerBuildItem build(List servlets, } } + String scrMainJava = null; + List knownClasses = null; + if (loggingDecorateBuildItem.isPresent()) { + scrMainJava = loggingDecorateBuildItem.get().getSrcMainJava().toString(); + knownClasses = loggingDecorateBuildItem.get().getKnowClasses(); + } + return new ServletDeploymentManagerBuildItem( - recorder.bootServletContainer(deployment, bc.getValue(), launchMode.getLaunchMode(), shutdownContext)); + recorder.bootServletContainer(deployment, + bc.getValue(), + launchMode.getLaunchMode(), + shutdownContext, + logBuildTimeConfig.decorateStacktraces, + scrMainJava, + knownClasses)); } diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java index 19f63cc1bf6f5..798475c142eac 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java @@ -4,6 +4,9 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; @@ -16,6 +19,9 @@ public class QuarkusErrorServlet extends HttpServlet { public static final String SHOW_STACK = "show-stack"; + public static final String SHOW_DECORATION = "show-decoration"; + public static final String SRC_MAIN_JAVA = "src-main-java"; + public static final String KNOWN_CLASSES = "known-classes"; @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { @@ -53,6 +59,16 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws if (showStack && exception != null) { htmlBuilder.stack(exception); } + final boolean showDecoration = Boolean.parseBoolean(getInitParameter(SHOW_DECORATION)); + final String srcMainJava = getInitParameter(SRC_MAIN_JAVA); + final String knownClassesString = getInitParameter(KNOWN_CLASSES); + List knownClasses = null; + if (knownClassesString != null) { + knownClasses = new ArrayList<>(Arrays.asList(knownClassesString.split(","))); + } + if (showDecoration && exception != null && srcMainJava != null && knownClasses != null) { + htmlBuilder.decorate(exception, srcMainJava, knownClasses); + } resp.getWriter().write(htmlBuilder.toString()); } } diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java index fb00e1a9bd6cb..5b125f47f90da 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java @@ -459,7 +459,8 @@ public Supplier servletContextSupplier() { } public DeploymentManager bootServletContainer(RuntimeValue info, BeanContainer beanContainer, - LaunchMode launchMode, ShutdownContext shutdownContext) { + LaunchMode launchMode, ShutdownContext shutdownContext, boolean decorateStacktrace, String scrMainJava, + List knownClasses) { if (info.getValue().getExceptionHandler() == null) { //if a 500 error page has not been mapped we change the default to our more modern one, with a UID in the //log. If this is not production we also include the stack trace @@ -475,9 +476,16 @@ public DeploymentManager bootServletContainer(RuntimeValue info, if (!alreadyMapped500 || launchMode.isDevOrTest()) { info.getValue().setExceptionHandler(new QuarkusExceptionHandler()); info.getValue().addErrorPage(new ErrorPage("/@QuarkusError", StatusCodes.INTERNAL_SERVER_ERROR)); + String knownClassesString = null; + if (knownClasses != null) + knownClassesString = String.join(",", knownClasses); info.getValue().addServlet(new ServletInfo("@QuarkusError", QuarkusErrorServlet.class) .addMapping("/@QuarkusError").setAsyncSupported(true) - .addInitParam(QuarkusErrorServlet.SHOW_STACK, Boolean.toString(launchMode.isDevOrTest()))); + .addInitParam(QuarkusErrorServlet.SHOW_STACK, Boolean.toString(launchMode.isDevOrTest())) + .addInitParam(QuarkusErrorServlet.SHOW_DECORATION, + Boolean.toString(decorateStacktrace(launchMode, decorateStacktrace))) + .addInitParam(QuarkusErrorServlet.SRC_MAIN_JAVA, scrMainJava) + .addInitParam(QuarkusErrorServlet.KNOWN_CLASSES, knownClassesString)); } if (!alreadyMapped404 && launchMode.equals(LaunchMode.DEVELOPMENT)) { info.getValue().addErrorPage(new ErrorPage("/@QuarkusNotFound", StatusCodes.NOT_FOUND)); @@ -764,6 +772,10 @@ public void addErrorPage(RuntimeValue deployment, String locatio deployment.getValue().addErrorPage(new ErrorPage(location, exceptionType)); } + private boolean decorateStacktrace(LaunchMode launchMode, boolean decorateStacktrace) { + return decorateStacktrace && launchMode.equals(LaunchMode.DEVELOPMENT); + } + /** * we can't have SecureRandom in the native image heap, so we need to lazy init */ diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java index 0f772ff344f08..ffee04566e460 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java @@ -15,12 +15,16 @@ import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.ide.EffectiveIdeBuildItem; import io.quarkus.deployment.ide.Ide; import io.quarkus.devui.spi.buildtime.BuildTimeActionBuildItem; import io.quarkus.utilities.OS; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.runtime.ide.IdeRecorder; /** * Processor for Ide interaction in Dev UI @@ -30,12 +34,23 @@ public class IdeProcessor { private static final Map LANG_TO_EXT = Map.of("java", "java", "kotlin", "kt"); @BuildStep(onlyIf = IsDevelopment.class) - void createJsonRPCService(BuildProducer buildTimeActionProducer, - Optional effectiveIdeBuildItem) { + @Record(ExecutionTime.RUNTIME_INIT) + void createOpenInIDEService(BuildProducer buildTimeActionProducer, + BuildProducer routeProducer, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + Optional effectiveIdeBuildItem, + IdeRecorder recorder) { if (effectiveIdeBuildItem.isPresent()) { Ide ide = effectiveIdeBuildItem.get().getIde(); if (ide != null) { + // For normal links (like from the error page) + routeProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() + .route("open-in-ide/:fileName/:lang/:lineNumber") + .handler(recorder.openInIde()) + .build()); + + // For Dev UI (like from the server log) BuildTimeActionBuildItem ideActions = new BuildTimeActionBuildItem(NAMESPACE); ideActions.addAction("open", map -> { diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java index 0f8d232660a8d..f02835a6ffae8 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java @@ -13,6 +13,7 @@ import io.quarkus.deployment.builditem.StreamingLogHandlerBuildItem; import io.quarkus.deployment.dev.RuntimeUpdatesProcessor; import io.quarkus.deployment.dev.testing.TestSupport; +import io.quarkus.deployment.logging.LoggingDecorateBuildItem; import io.quarkus.dev.spi.DevModeType; import io.quarkus.devui.runtime.logstream.LogStreamBroadcaster; import io.quarkus.devui.runtime.logstream.LogStreamJsonRPCService; @@ -21,6 +22,7 @@ import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; import io.quarkus.devui.spi.buildtime.BuildTimeActionBuildItem; import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.logging.LogBuildTimeConfig; /** * Processor for Log stream in Dev UI @@ -40,8 +42,12 @@ void additionalBean(BuildProducer additionalBeanProduce @Record(ExecutionTime.STATIC_INIT) @SuppressWarnings("unchecked") public void handler(BuildProducer streamingLogHandlerBuildItem, + LogBuildTimeConfig logBuildTimeConfig, + LoggingDecorateBuildItem loggingDecorateBuildItem, LogStreamRecorder recorder) { - RuntimeValue> mutinyLogHandler = recorder.mutinyLogHandler(); + RuntimeValue> mutinyLogHandler = recorder.mutinyLogHandler( + logBuildTimeConfig.decorateStacktraces, loggingDecorateBuildItem.getSrcMainJava().toString(), + loggingDecorateBuildItem.getKnowClasses()); streamingLogHandlerBuildItem.produce(new StreamingLogHandlerBuildItem((RuntimeValue) mutinyLogHandler)); } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index 53d8bfb19f20f..78637f8fdee2d 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -45,6 +45,7 @@ import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; +import io.quarkus.deployment.logging.LoggingDecorateBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageRunnerBuildItem; import io.quarkus.deployment.pkg.steps.GraalVM; import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; @@ -55,6 +56,7 @@ import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.LiveReloadConfig; import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.logging.LogBuildTimeConfig; import io.quarkus.runtime.shutdown.ShutdownConfig; import io.quarkus.tls.TlsRegistryBuildItem; import io.quarkus.utilities.OS; @@ -327,7 +329,8 @@ BodyHandlerBuildItem bodyHandler(VertxHttpRecorder recorder) { @BuildStep @Record(ExecutionTime.RUNTIME_INIT) - ServiceStartBuildItem finalizeRouter( + ServiceStartBuildItem finalizeRouter(Optional decorateBuildItem, + LogBuildTimeConfig logBuildTimeConfig, VertxHttpRecorder recorder, BeanContainerBuildItem beanContainer, CoreVertxBuildItem vertx, LaunchModeBuildItem launchMode, List defaultRoutes, @@ -399,6 +402,13 @@ ServiceStartBuildItem finalizeRouter( combinedActions.addAll(errorPageActionsBuildItem.getActions()); } + String srcMainJava = null; + List knowClasses = null; + if (decorateBuildItem.isPresent()) { + srcMainJava = decorateBuildItem.get().getSrcMainJava().toString(); + knowClasses = decorateBuildItem.get().getKnowClasses(); + } + recorder.finalizeRouter(beanContainer.getValue(), defaultRoute.map(DefaultRouteBuildItem::getRoute).orElse(null), listOfFilters, listOfManagementInterfaceFilters, @@ -410,6 +420,9 @@ ServiceStartBuildItem finalizeRouter( launchMode.getLaunchMode(), getBodyHandlerRequiredConditions(requireBodyHandlerBuildItems), bodyHandlerBuildItem.getHandler(), gracefulShutdownFilter, shutdownConfig, executorBuildItem.getExecutorProxy(), + logBuildTimeConfig, + srcMainJava, + knowClasses, combinedActions); return new ServiceStartBuildItem("vertx-http"); diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java index 698376e023e14..723d445015074 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java @@ -242,7 +242,8 @@ private Matcher htmlBodyMatcher() { containsString(""), containsString("Internal Server Error"), containsString("java.lang.RuntimeException: Simulated failure"), - containsString("at " + BeanRegisteringRouteThatThrowsException.class.getName() + "$1.handle")); + containsString(BeanRegisteringRouteThatThrowsException.class.getName() + "$1"), + containsString(".handle")); } @ApplicationScoped diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js index 838189ff4e6df..1fc0bfca87d2a 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js @@ -262,7 +262,7 @@ export class QwcServerLog extends QwcAbstractLogElement { ${this._renderProcessName(message.processName)} ${this._renderThreadId(message.threadId)} ${this._renderThreadName(message.threadName)} - ${this._renderMessage(level, message.formattedMessage, message.stacktrace)} + ${this._renderMessage(level, message.formattedMessage, message.stacktrace, message.decoration)} `; } } @@ -403,7 +403,7 @@ export class QwcServerLog extends QwcAbstractLogElement { } } - _renderMessage(level, message, stacktrace){ + _renderMessage(level, message, stacktrace, decoration){ if(this._selectedColumns.includes('19')){ // Clean up Ansi message = message.replace(/\u001b\[.*?m/g, ""); @@ -417,27 +417,37 @@ export class QwcServerLog extends QwcAbstractLogElement { } // Make sure multi line is supported - if(message.includes('\n')){ - var htmlifiedLines = []; - var lines = message.split('\n'); - for (var i = 0; i < lines.length; i++) { - var line = lines[i]; - line = line.replace(/ /g, '\u00a0'); - if(i === lines.length-1){ - htmlifiedLines.push(line); - }else{ - htmlifiedLines.push(line + '<br/>'); - } - } - message = htmlifiedLines.join(''); - } + message = this._makeMultiLine(message); if(message){ - return html`<span title="Message" class='text-${level}'>${unsafeHTML(message)}${this._renderStackTrace(stacktrace)}</span>`; + return html`<span title="Message" class='text-${level}'>${unsafeHTML(message)}${this._renderDecoration(decoration)}${this._renderStackTrace(stacktrace)}</span>`; + } + } + } + + _renderDecoration(decoration){ + if(decoration){ + decoration = this._makeMultiLine("\n" + decoration + "\n"); + return html`${unsafeHTML(decoration)}`; + } + } + + _makeMultiLine(message){ + if(message.includes('\n')){ + var htmlifiedLines = []; + var lines = message.split('\n'); + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + line = line.replace(/ /g, '\u00a0'); + if(i === lines.length-1){ + htmlifiedLines.push(line); + }else{ + htmlifiedLines.push(line + '<br/>'); + } } - - + message = htmlifiedLines.join(''); } + return message; } _renderStackTrace(stacktrace){ diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamRecorder.java index 989be2fc5ef06..ffb79a3a19a48 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamRecorder.java @@ -1,5 +1,6 @@ package io.quarkus.devui.runtime.logstream; +import java.util.List; import java.util.Optional; import io.quarkus.runtime.RuntimeValue; @@ -8,8 +9,9 @@ @Recorder public class LogStreamRecorder { - public RuntimeValue<Optional<MutinyLogHandler>> mutinyLogHandler() { - return new RuntimeValue<>(Optional.of(new MutinyLogHandler())); + public RuntimeValue<Optional<MutinyLogHandler>> mutinyLogHandler(boolean decorateStack, String srcMainJava, + List<String> knownClasses) { + return new RuntimeValue<>(Optional.of(new MutinyLogHandler(decorateStack, srcMainJava, knownClasses))); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/MutinyLogHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/MutinyLogHandler.java index dbe414c9db0de..9454c04fa316b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/MutinyLogHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/MutinyLogHandler.java @@ -1,9 +1,12 @@ package io.quarkus.devui.runtime.logstream; +import java.util.List; + import org.jboss.logmanager.ExtHandler; import org.jboss.logmanager.ExtLogRecord; import io.quarkus.arc.Arc; +import io.quarkus.runtime.logging.DecorateStackUtil; import io.vertx.core.json.JsonObject; /** @@ -12,8 +15,14 @@ public class MutinyLogHandler extends ExtHandler { private LogStreamBroadcaster logStreamBroadcaster; - - public MutinyLogHandler() { + private final boolean decorateStack; + private final String srcMainJava; + private final List<String> knownClasses; + + public MutinyLogHandler(boolean decorateStack, String srcMainJava, List<String> knownClasses) { + this.decorateStack = decorateStack; + this.srcMainJava = srcMainJava; + this.knownClasses = knownClasses; setFormatter(new JsonFormatter()); } @@ -28,6 +37,14 @@ public final void doPublish(final ExtLogRecord record) { LogStreamBroadcaster broadcaster = getBroadcaster(); if (broadcaster != null) { JsonObject message = ((JsonFormatter) getFormatter()).toJsonObject(record); + if (decorateStack) { + String decoratedString = DecorateStackUtil.getDecoratedString(record.getThrown(), this.srcMainJava, + knownClasses); + if (decoratedString != null) { + message.put("decoration", decoratedString); + } + } + broadcaster.onNext(message); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/DecoratedAssertionError.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/DecoratedAssertionError.java deleted file mode 100644 index 85848a9949aba..0000000000000 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/DecoratedAssertionError.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.quarkus.vertx.http.runtime; - -import java.io.PrintStream; -import java.io.PrintWriter; - -/** - * Wraps an exception and prints stack trace with code snippets. - * - * <p> - * To obtain the original exception, use {@link #getOriginal()}. - * - * <p> - * NOTE: Copied from <a href="https://github.com/laech/java-stacksrc">laech/java-stacksrc</a> - */ -final class DecoratedAssertionError extends AssertionError { - - private final Throwable original; - private final String decoratedStackTrace; - - DecoratedAssertionError(Throwable original) { - this(original, null); - } - - /** - * @param pruneStackTraceKeepFromClass if not null, will prune the stack traces, keeping only - * elements that are called directly or indirectly by this class - */ - DecoratedAssertionError( - Throwable original, Class<?> pruneStackTraceKeepFromClass) { - this.original = original; - this.decoratedStackTrace = StackTraceDecorator.get().decorate(original, pruneStackTraceKeepFromClass); - setStackTrace(new StackTraceElement[0]); - } - - @Override - public String getMessage() { - // Override this instead of calling the super(message) constructor, as super(null) will create - // the "null" string instead of actually being null - return getOriginal().getMessage(); - } - - /** Gets the original throwable being wrapped. */ - public Throwable getOriginal() { - return original; - } - - @Override - public void printStackTrace(PrintWriter out) { - out.println(this); - } - - @Override - public void printStackTrace(PrintStream out) { - out.println(this); - } - - @Override - public String toString() { - return decoratedStackTrace; - } -} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FileCollector.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FileCollector.java deleted file mode 100644 index 8fb5e5bd6dbce..0000000000000 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/FileCollector.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.quarkus.vertx.http.runtime; - -import java.io.IOException; -import java.nio.file.FileVisitOption; -import java.nio.file.FileVisitResult; -import java.nio.file.FileVisitor; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * NOTE: Copied from <a href="https://github.com/laech/java-stacksrc">laech/java-stacksrc</a> - */ -final class FileCollector implements FileVisitor<Path> { - - private final Map<String, List<Path>> result = new HashMap<>(); - - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - result.computeIfAbsent(file.getFileName().toString(), __ -> new ArrayList<>()).add(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFileFailed(Path file, IOException exc) { - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) { - return FileVisitResult.CONTINUE; - } - - static Map<String, List<Path>> collect(Path dir) throws IOException { - var collector = new FileCollector(); - Files.walkFileTree(dir, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, collector); - return collector.result; - } -} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index 2e581bcb613d9..8a5465f671650 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -289,12 +289,6 @@ public class HttpConfiguration { @ConfigItem public Map<String, FilterConfig> filter; - /** - * 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; - public ProxyConfig proxy; public int determinePort(LaunchMode launchMode) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java index 8405fe52bfe00..7e7021abd8ecf 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java @@ -45,18 +45,24 @@ public class QuarkusErrorHandler implements Handler<RoutingContext> { private final boolean decorateStack; private final Optional<HttpConfiguration.PayloadHint> contentTypeDefault; private final List<ErrorPageAction> actions; + private final List<String> knowClasses; + private final String srcMainJava; public QuarkusErrorHandler(boolean showStack, boolean decorateStack, Optional<HttpConfiguration.PayloadHint> contentTypeDefault) { - this(showStack, decorateStack, contentTypeDefault, List.of()); + this(showStack, decorateStack, contentTypeDefault, null, List.of(), List.of()); } public QuarkusErrorHandler(boolean showStack, boolean decorateStack, Optional<HttpConfiguration.PayloadHint> contentTypeDefault, + String srcMainJava, + List<String> knowClasses, List<ErrorPageAction> actions) { this.showStack = showStack; this.decorateStack = decorateStack; this.contentTypeDefault = contentTypeDefault; + this.srcMainJava = srcMainJava; + this.knowClasses = knowClasses; this.actions = actions; } @@ -141,9 +147,6 @@ public void accept(Throwable throwable) { exception.addSuppressed(e); } if (showStack && exception != null) { - if (decorateStack) { - exception = new DecoratedAssertionError(exception); - } details = generateHeaderMessage(exception, uuid); stack = generateStackTrace(exception); } else { @@ -211,8 +214,12 @@ private void htmlResponse(RoutingContext event, String details, Throwable except event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8"); final TemplateHtmlBuilder htmlBuilder = new TemplateHtmlBuilder("Internal Server Error", details, details, this.actions); + + if (decorateStack && exception != null) { + htmlBuilder.decorate(exception, this.srcMainJava, this.knowClasses); + } if (showStack && exception != null) { - htmlBuilder.stack(exception); + htmlBuilder.stack(exception, this.knowClasses); } writeResponse(event, htmlBuilder.toString()); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StackTraceDecorator.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StackTraceDecorator.java deleted file mode 100644 index 2f0a1a4b32c37..0000000000000 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StackTraceDecorator.java +++ /dev/null @@ -1,235 +0,0 @@ -package io.quarkus.vertx.http.runtime; - -import static java.lang.String.format; -import static java.lang.System.lineSeparator; -import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toList; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.NavigableMap; -import java.util.Optional; -import java.util.Set; -import java.util.TreeMap; -import java.util.stream.IntStream; - -/** - * <p> - * NOTE: Copied from <a href="https://github.com/laech/java-stacksrc">laech/java-stacksrc</a> - */ -final class StackTraceDecorator { - private StackTraceDecorator() { - } - - private static final StackTraceDecorator instance = new StackTraceDecorator(); - - public static StackTraceDecorator get() { - return instance; - } - - private static final int CONTEXT_LINE_COUNT = 2; - - private volatile Map<String, List<Path>> cachedFiles; - - private Map<String, List<Path>> cachedFiles() throws IOException { - if (cachedFiles == null) { - cachedFiles = FileCollector.collect(Paths.get("")); - } - return cachedFiles; - } - - public String decorate(Throwable e) { - return decorate(e, null); - } - - public String decorate(Throwable e, Class<?> keepFromClass) { - if (keepFromClass != null) { - pruneStackTrace(e, keepFromClass, new HashSet<>()); - } - - var stackTrace = getStackTraceAsString(e); - try { - - var alreadySeenElements = new HashSet<StackTraceElement>(); - var alreadySeenSnippets = new HashSet<List<String>>(); - stackTrace = decorate(e, stackTrace, 1, alreadySeenElements, alreadySeenSnippets); - - var cause = e.getCause(); - if (cause != null) { - stackTrace = decorate(cause, stackTrace, 1, alreadySeenElements, alreadySeenSnippets); - } - - for (var suppressed : e.getSuppressed()) { - stackTrace = decorate(suppressed, stackTrace, 2, alreadySeenElements, alreadySeenSnippets); - } - - } catch (Exception sup) { - e.addSuppressed(sup); - } - return stackTrace; - } - - private String decorate( - Throwable e, - String stackTrace, - int indentLevel, - Set<StackTraceElement> alreadySeenElements, - Set<List<String>> alreadySeenSnippets) - throws IOException { - - for (var element : e.getStackTrace()) { - if (!alreadySeenElements.add(element)) { - continue; - } - - var snippet = decorate(element); - if (snippet.isEmpty() || !alreadySeenSnippets.add(snippet.get())) { - // Don't print the same snippet multiple times, - // multiple lambda on one line can create this situation - continue; - } - - var line = element.toString(); - var indent = "\t".repeat(indentLevel); - var replacement = String.format( - "%s%n%n%s%n%n", - line, snippet.get().stream().collect(joining(lineSeparator() + indent, indent, ""))); - stackTrace = stackTrace.replace(line, replacement); - } - return stackTrace; - } - - private Optional<List<String>> decorate(StackTraceElement element) throws IOException { - var file = findFile(element); - if (file.isEmpty()) { - return Optional.empty(); - } - - var lines = readContextLines(element, file.get()); - if (lines.isEmpty()) { - return Optional.empty(); - } - - removeBlankLinesFromStart(lines); - removeBlankLinesFromEnd(lines); - return Optional.of(buildSnippet(lines, element)); - } - - private Optional<Path> findFile(StackTraceElement element) throws IOException { - if (element.getLineNumber() < 1 - || element.getFileName() == null - || element.getMethodName().startsWith("access$")) { // Ignore class entry lines - return Optional.empty(); - } - - var tail = withPackagePath(element); - var paths = cachedFiles().getOrDefault(element.getFileName(), List.of()); - var exact = paths.stream().filter(it -> it.endsWith(tail)).findAny(); - if (exact.isPresent() || element.getFileName().endsWith(".java")) { - return exact; - } - return Optional.ofNullable(paths.size() == 1 ? paths.get(0) : null); - } - - private Path withPackagePath(StackTraceElement element) { - var fileName = requireNonNull(element.getFileName()); - var className = element.getClassName(); - var i = className.lastIndexOf("."); - var parent = i < 0 ? "" : className.substring(0, i).replace('.', '/'); - return Paths.get(parent).resolve(fileName); - } - - private NavigableMap<Integer, String> readContextLines(StackTraceElement elem, Path path) - throws IOException { - - var startLineNum = Math.max(1, elem.getLineNumber() - CONTEXT_LINE_COUNT); - try (var stream = Files.lines(path)) { - - var lines = stream - .limit(elem.getLineNumber() + CONTEXT_LINE_COUNT) - .skip(startLineNum - 1) - .collect(toList()); - - return IntStream.range(0, lines.size()) - .boxed() - .reduce( - new TreeMap<>(), - (acc, i) -> { - acc.put(i + startLineNum, lines.get(i)); - return acc; - }, - (a, b) -> b); - } - } - - @SuppressWarnings("NullAway") - private void removeBlankLinesFromStart(NavigableMap<Integer, String> lines) { - IntStream.rangeClosed(lines.firstKey(), lines.lastKey()) - .takeWhile(i -> lines.get(i).isBlank()) - .forEach(lines::remove); - } - - @SuppressWarnings("NullAway") - private void removeBlankLinesFromEnd(NavigableMap<Integer, String> lines) { - IntStream.iterate(lines.lastKey(), i -> i >= lines.firstKey(), i -> i - 1) - .takeWhile(i -> lines.get(i).isBlank()) - .forEach(lines::remove); - } - - private static List<String> buildSnippet( - NavigableMap<Integer, String> lines, StackTraceElement elem) { - var maxLineNumWidth = String.valueOf(lines.lastKey()).length(); - return lines.entrySet().stream() - .map( - entry -> { - var lineNum = entry.getKey(); - var isTarget = lineNum == elem.getLineNumber(); - var line = entry.getValue(); - var lineNumStr = format("%" + maxLineNumWidth + "d", lineNum); - return format( - "%s %s%s", isTarget ? "->" : " ", lineNumStr, line.isEmpty() ? "" : " " + line); - }) - .collect(toList()); - } - - private static String getStackTraceAsString(Throwable e) { - var stringWriter = new StringWriter(); - var printWriter = new PrintWriter(stringWriter); - e.printStackTrace(printWriter); - printWriter.flush(); - return stringWriter.toString(); - } - - private static void pruneStackTrace( - Throwable throwable, Class<?> keepFromClass, Set<Throwable> alreadySeen) { - if (!alreadySeen.add(throwable)) { - return; - } - - var stackTrace = throwable.getStackTrace(); - for (var i = stackTrace.length - 1; i >= 0; i--) { - if (stackTrace[i].getClassName().equals(keepFromClass.getName())) { - throwable.setStackTrace(Arrays.copyOfRange(stackTrace, 0, i + 1)); - break; - } - } - - var cause = throwable.getCause(); - if (cause != null) { - pruneStackTrace(cause, keepFromClass, alreadySeen); - } - - for (var suppressed : throwable.getSuppressed()) { - pruneStackTrace(suppressed, keepFromClass, alreadySeen); - } - } -} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index 156aabc83880f..a3d8ebd792d96 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -65,6 +65,7 @@ import io.quarkus.runtime.configuration.ConfigInstantiator; import io.quarkus.runtime.configuration.ConfigUtils; import io.quarkus.runtime.configuration.MemorySize; +import io.quarkus.runtime.logging.LogBuildTimeConfig; import io.quarkus.runtime.shutdown.ShutdownConfig; import io.quarkus.tls.TlsConfigurationRegistry; import io.quarkus.tls.runtime.config.TlsConfig; @@ -379,6 +380,9 @@ public void finalizeRouter(BeanContainer container, Consumer<Route> defaultRoute Handler<RoutingContext> bodyHandler, GracefulShutdownFilter gracefulShutdownFilter, ShutdownConfig shutdownConfig, Executor executor, + LogBuildTimeConfig logBuildTimeConfig, + String srcMainJava, + List<String> knowClasses, List<ErrorPageAction> actions) { HttpConfiguration httpConfiguration = this.httpConfiguration.getValue(); // install the default route at the end @@ -416,8 +420,8 @@ public void finalizeRouter(BeanContainer container, Consumer<Route> defaultRoute applyCompression(httpBuildTimeConfig.enableCompression, httpRouteRouter); httpRouteRouter.route().last().failureHandler( - new QuarkusErrorHandler(launchMode.isDevOrTest(), decorateStacktrace(launchMode, httpConfiguration), - httpConfiguration.unhandledErrorContentTypeDefault, actions)); + new QuarkusErrorHandler(launchMode.isDevOrTest(), decorateStacktrace(launchMode, logBuildTimeConfig), + httpConfiguration.unhandledErrorContentTypeDefault, srcMainJava, knowClasses, actions)); for (BooleanSupplier requireBodyHandlerCondition : requireBodyHandlerConditions) { if (requireBodyHandlerCondition.getAsBoolean()) { //if this is set then everything needs the body handler installed @@ -535,8 +539,8 @@ public void handle(RoutingContext event) { addHotReplacementHandlerIfNeeded(mr); mr.route().last().failureHandler( - new QuarkusErrorHandler(launchMode.isDevOrTest(), decorateStacktrace(launchMode, httpConfiguration), - httpConfiguration.unhandledErrorContentTypeDefault, actions)); + new QuarkusErrorHandler(launchMode.isDevOrTest(), decorateStacktrace(launchMode, logBuildTimeConfig), + httpConfiguration.unhandledErrorContentTypeDefault, srcMainJava, knowClasses, actions)); mr.route().order(RouteConstants.ROUTE_ORDER_BODY_HANDLER_MANAGEMENT) .handler(createBodyHandlerForManagementInterface()); @@ -577,8 +581,8 @@ public void handle(HttpServerRequest event) { } } - private boolean decorateStacktrace(LaunchMode launchMode, HttpConfiguration httpConfiguration) { - return httpConfiguration.decorateStacktraces && launchMode.equals(LaunchMode.DEVELOPMENT); + private boolean decorateStacktrace(LaunchMode launchMode, LogBuildTimeConfig logBuildTimeConfig) { + return logBuildTimeConfig.decorateStacktraces && launchMode.equals(LaunchMode.DEVELOPMENT); } private void addHotReplacementHandlerIfNeeded(Router router) { diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ide/IdeRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ide/IdeRecorder.java new file mode 100644 index 0000000000000..ee3ed92836343 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ide/IdeRecorder.java @@ -0,0 +1,23 @@ +package io.quarkus.vertx.http.runtime.ide; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.runtime.annotations.Recorder; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +@Recorder +public class IdeRecorder { + + public Handler<RoutingContext> openInIde() { + return new Handler<RoutingContext>() { + @Override + public void handle(RoutingContext rc) { + DevConsoleManager.invoke("devui-ide-interaction.open", rc.pathParams()); + rc.response() + .setStatusCode(HttpResponseStatus.ACCEPTED.code()).end(); + } + }; + } + +}