Skip to content
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
6 changes: 4 additions & 2 deletions docs/asciidoc/modules/modules.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ Modules are distributed as separate dependencies. Below is the catalog of offici
==== Development Tools
* link:{uiVersion}/#tooling-and-operations-development[Jooby Run]: Run and hot reload your application.
* link:{uiVersion}/modules/whoops[Whoops]: Pretty page stacktrace reporter.
* link:{uiVersion}/modules/metrics[Metrics]: Application metrics from the excellent metrics library.
* link:{uiVersion}/modules/opentelemetry[Open Telemetry]: Application metrics using Open Telemetry library.

==== Event Bus
* link:{uiVersion}/modules/camel[Camel]: Camel module for Jooby.
Expand All @@ -51,6 +49,10 @@ Modules are distributed as separate dependencies. Below is the catalog of offici
* link:{uiVersion}/modules/jackson3[Jackson3]: Jackson3 module for Jooby.
* link:{uiVersion}/modules/yasson[Yasson]: JSON-B module for Jooby.

==== Metrics
* link:{uiVersion}/modules/metrics[Metrics]: Application metrics from the excellent metrics library.
* link:{uiVersion}/modules/opentelemetry[Open Telemetry]: Application metrics using Open Telemetry library.

==== OpenAPI
* link:{uiVersion}/modules/openapi[OpenAPI]: OpenAPI supports.

Expand Down
Binary file modified docs/images/whoops.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions docs/js/styles/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -806,3 +806,10 @@ html, body {
overflow-wrap: break-word;
word-break: break-word;
}

/* Responsive images */
#content img {
max-width: 100%;
height: auto;
border-radius: var(--border-radius);
}
7 changes: 1 addition & 6 deletions modules/jooby-whoops/NOTE.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
To make a new release:

- https://github.com/filp/whoops/tags
- This repo used 2.15.4
- Unzip the release
- copy *.php files to test/resources/whoops/views
- Run GenerateHTML
- Verify all php lines were converted (manually)
- Run WhoopsTest
- This repo used 2.18.4
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,36 @@
import static java.util.Optional.ofNullable;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Frame {
private static final int SAMPLE_SIZE = 10;

private static final Set<String> VENDOR_PACKAGES =
new HashSet<>(
Arrays.asList(
"java.",
"javax.",
"jakarta.",
"sun.",
"com.sun.",
"org.eclipse.",
"io.netty.",
"org.hibernate.",
"com.fasterxml.",
"tools.jackson.",
"org.slf4j",
"io.undertow.",
"io.jooby."));

private String fileName;

private String methodName;
Expand Down Expand Up @@ -75,7 +95,15 @@ public List<Throwable> getComments() {
}

public boolean hasSource() {
return source != null && source.length() > 0;
return source != null && !source.isEmpty();
}

public boolean shouldScan() {
return VENDOR_PACKAGES.stream().noneMatch(it -> getClassName().startsWith(it));
}

public boolean isApplication() {
return hasSource();
}

public static List<Frame> toFrames(SourceLocator locator, Throwable cause) {
Expand All @@ -89,8 +117,7 @@ public static List<Frame> toFrames(SourceLocator locator, Throwable cause) {

Stream.of(head.getStackTrace()).map(e -> toFrame(locator, head, e)).forEach(frames::add);

// Keep application frames (ignore all others)
return frames.stream().filter(Frame::hasSource).collect(Collectors.toList());
return frames.stream().toList();
}

static Frame toFrame(
Expand All @@ -100,28 +127,37 @@ static Frame toFrame(
String[] names = className.split("\\.");
String filename = ofNullable(e.getFileName()).orElse(names[names.length - 1]);

StringBuilder path = new StringBuilder();
Stream.of(names).limit(names.length - 1).forEach(it -> path.append(it).append(File.separator));
path.append(names[names.length - 1]);
SourceLocator.Source source = locator.source(path.toString());
SourceLocator.Preview preview = source.preview(line, SAMPLE_SIZE);

Frame frame = new Frame();
frame.fileName = filename;
frame.methodName = ofNullable(e.getMethodName()).orElse("~unknown");
frame.lineStart = preview.getLineStart();
frame.line = line;
frame.location =
Files.exists(source.getPath())
? locator.getBasedir().relativize(source.getPath()).toString()
: filename;
frame.source = preview.getCode();
frame.open = false;
frame.className =
className
// clean up kotlin generated class name: App$1$1 => App
.replaceAll("\\$\\d+", "");
frame.comments = Collections.singletonList(cause);

if (frame.shouldScan()) {
StringBuilder path = new StringBuilder();
Stream.of(names)
.limit(names.length - 1)
.forEach(it -> path.append(it).append(File.separator));
path.append(names[names.length - 1]);
SourceLocator.Source source = locator.source(path.toString());
SourceLocator.Preview preview = source.preview(line, SAMPLE_SIZE);

frame.lineStart = preview.getLineStart();
Path sourcePath = source.getPath();
frame.location =
sourcePath != null && sourcePath.isAbsolute()
? locator.getBasedir().relativize(sourcePath).toString()
: filename;
frame.source = preview.getCode();
} else {
frame.location = filename;
}

frame.open = false;
return frame;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
package io.jooby.internal.whoops;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
Expand All @@ -16,7 +17,6 @@
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -49,8 +49,6 @@ public String getCode() {
}

public static class Source {
private static final int[] RANGE = {0, 0};

private final Path path;

public Source(final Path path) {
Expand All @@ -62,42 +60,35 @@ public Path getPath() {
}

public Preview preview(final int line, final int size) {
List<String> lines = getLines();

int[] range = range(line, size, lines.size());
int from = range[0];
int to = range[1];

String code;
if (from >= 0 && to <= lines.size()) {
code =
lines.subList(from, to).stream()
.map(l -> l.length() == 0 ? " " : l)
.collect(Collectors.joining("\n"));
} else {
code = "";
int start = Math.max(1, line - size);
int end = line + size;
List<String> codeLines = new ArrayList<>();
int lineStart = -1;

try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String l;
int n = 1;
while ((l = reader.readLine()) != null) {
if (n >= start) {
if (lineStart == -1) {
lineStart = n;
}
codeLines.add(l.length() == 0 ? " " : l);
}
if (n >= end) {
break;
}
n++;
}
} catch (IOException x) {
return new Preview("", 1);
}
return new Preview(code, from + 1);
}

private int[] range(final int line, final int size, int totalSize) {
if (line < totalSize) {
int from = Math.max(line - size, 0);
int toset = Math.max((line - from) - size, 0);
int to = Math.min(from + toset + size * 2, totalSize);
int fromset = Math.abs((to - line) - size);
from = Math.max(from - fromset, 0);
return new int[] {from, to};
if (lineStart == -1) {
return new Preview("", 1);
}
return RANGE;
}

private List<String> getLines() {
try {
return Files.readAllLines(path, StandardCharsets.UTF_8);
} catch (IOException x) {
return Collections.emptyList();
}
return new Preview(String.join("\n", codeLines), lineStart);
}

@Override
Expand All @@ -111,6 +102,8 @@ public String toString() {
private Path basedir;

private Map<String, Source> sources = new ConcurrentHashMap<>();
private final Map<String, List<Path>> sourceIndex = new ConcurrentHashMap<>();
private volatile boolean indexBuilt = false;

public SourceLocator(Path basedir) {
this.basedir = basedir;
Expand All @@ -124,58 +117,78 @@ public Source source(String filename) {
return sources.computeIfAbsent(
filename,
f -> {
Set<String> skip =
Stream.of("target", "bin", "build", "tmp", "temp", "node_modules", "node")
.collect(Collectors.toSet());
try {
List<String> files =
Arrays.asList(
filename,
filename.replace(".", File.separator) + ".java",
filename.replace(".", File.separator) + ".kt",
filename.replace(".", File.separator) + "Kt.kt");
List<Path> source = new ArrayList<>();
source.add(Paths.get(filename));
log.debug("scanning {}", basedir);
Files.walkFileTree(
basedir,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(
final Path dir, final BasicFileAttributes attrs) throws IOException {
String dirName = dir.getFileName().toString();
if (Files.isHidden(dir) || dirName.startsWith(".")) {
log.debug("skipping hidden directory: {}", dir);
return FileVisitResult.SKIP_SUBTREE;
}
if (skip.contains(dirName)) {
log.debug("skipping binary directory: {}", dir);
return FileVisitResult.SKIP_SUBTREE;
}
log.debug("found directory: {}", dir);
return FileVisitResult.CONTINUE;
buildIndexIfNeeded();

String simpleName = f.substring(f.lastIndexOf(File.separator) + 1);
List<String> searchFiles =
Arrays.asList(simpleName + ".java", simpleName + ".kt", simpleName + "Kt.kt");

List<String> candidateSuffixes =
Arrays.asList(f + ".java", f + ".kt", f.replace(".", File.separator) + "Kt.kt");

for (String searchFile : searchFiles) {
List<Path> paths = sourceIndex.get(searchFile);
if (paths != null) {
for (Path path : paths) {
String pathStr = path.toString();
for (String suffix : candidateSuffixes) {
if (pathStr.endsWith(suffix)) {
return new Source(path);
}

@Override
public FileVisitResult visitFile(
final Path file, final BasicFileAttributes attrs) {
return files.stream()
.filter(f -> file.toString().endsWith(f))
.findFirst()
.map(
f -> {
source.add(0, file.toAbsolutePath());
return FileVisitResult.TERMINATE;
})
.orElse(FileVisitResult.CONTINUE);
}
});
return new Source(source.get(0));
} catch (IOException x) {
return new Source(Paths.get(filename));
} finally {
log.debug("done scanning {}", basedir);
}
}
}
}
return new Source(Paths.get(filename));
});
}

private void buildIndexIfNeeded() {
if (indexBuilt) {
return;
}
synchronized (this) {
if (indexBuilt) {
return;
}
try {
log.debug("scanning {}", basedir);
Set<String> skip =
Stream.of("target", "bin", "build", "tmp", "temp", "node_modules", "node")
.collect(Collectors.toSet());
Files.walkFileTree(
basedir,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(
final Path dir, final BasicFileAttributes attrs) throws IOException {
String dirName = dir.getFileName().toString();
if (Files.isHidden(dir) || dirName.startsWith(".")) {
log.debug("skipping hidden directory: {}", dir);
return FileVisitResult.SKIP_SUBTREE;
}
if (skip.contains(dirName)) {
log.debug("skipping binary directory: {}", dir);
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) {
String fileName = file.getFileName().toString();
if (fileName.endsWith(".java") || fileName.endsWith(".kt")) {
sourceIndex.computeIfAbsent(fileName, k -> new ArrayList<>()).add(file);
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException x) {
log.error("source index failed", x);
} finally {
log.debug("done scanning {}", basedir);
indexBuilt = true;
}
}
}
}
Loading
Loading