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
97 changes: 60 additions & 37 deletions core/src/main/java/dev/faststats/core/ErrorHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;

final class ErrorHelper {
private static final int MESSAGE_LENGTH = Math.min(1000, Integer.getInteger("faststats.message-length", 500));
Expand All @@ -20,42 +21,64 @@ public static JsonObject compile(final Throwable error, @Nullable final List<Str
final var report = new JsonObject();
final var message = getAnonymizedMessage(error);

report.addProperty("error", error.getClass().getName());
if (message != null) report.addProperty("message", message);
final var stacktrace = new JsonArray();
final var header = message != null
? error.getClass().getName() + ": " + message
: error.getClass().getName();
stacktrace.add(header);

final var elements = error.getStackTrace();
final var stack = collapseStackTrace(elements);
final var list = new ArrayList<>(stack);
if (suppress != null) list.removeAll(suppress);
final var traces = Math.min(list.size(), STACK_TRACE_LIMIT);

final var stacktrace = populateTraces(traces, list, elements);
if (!stacktrace.isEmpty()) report.add("stack", stacktrace);
populateTraces(traces, list, elements, stacktrace);
appendCauseChain(error.getCause(), stack, suppress, stacktrace);

if (error.getCause() != null) {
final var toSuppress = new ArrayList<>(stack);
if (suppress != null) toSuppress.addAll(suppress);
report.add("cause", compile(error.getCause(), toSuppress));
}
report.addProperty("error", error.getClass().getName());
if (message != null) report.addProperty("message", message);

report.add("stack", stacktrace);

return report;
}

private static JsonArray populateTraces(final int traces, final ArrayList<String> list, final StackTraceElement[] elements) {
final var stacktrace = new JsonArray(traces);
private static void appendCauseChain(@Nullable Throwable cause, final List<String> parentStack,
@Nullable final List<String> suppress, final JsonArray stacktrace) {
final var toSuppress = new ArrayList<>(parentStack);
if (suppress != null) toSuppress.addAll(suppress);
final var visited = Collections.<Throwable>newSetFromMap(new IdentityHashMap<>());
while (cause != null && visited.add(cause)) {
final var causeMessage = getAnonymizedMessage(cause);
final var header = causeMessage != null
? "Caused by: " + cause.getClass().getName() + ": " + causeMessage
: "Caused by: " + cause.getClass().getName();
stacktrace.add(header);

final var causeElements = cause.getStackTrace();
final var causeStack = collapseStackTrace(causeElements);
final var causeList = new ArrayList<>(causeStack);
causeList.removeAll(toSuppress);
final var causeTraces = Math.min(causeList.size(), STACK_TRACE_LIMIT);
populateTraces(causeTraces, causeList, causeElements, stacktrace);

cause = cause.getCause();
}
}

private static void populateTraces(final int traces, final List<String> list, final StackTraceElement[] elements, final JsonArray stacktrace) {
for (var i = 0; i < traces; i++) {
final var string = list.get(i);
if (string.length() <= STACK_TRACE_LENGTH) stacktrace.add(string);
else stacktrace.add(string.substring(0, STACK_TRACE_LENGTH) + "...");
if (string.length() <= STACK_TRACE_LENGTH) stacktrace.add(" at " + string);
else stacktrace.add(" at " + string.substring(0, STACK_TRACE_LENGTH) + "...");
}
if (traces > 0 && traces < list.size()) {
stacktrace.add("and " + (list.size() - traces) + " more...");
stacktrace.add(" ... " + (list.size() - traces) + " more");
} else {
final var i = elements.length - list.size();
if (i > 0) stacktrace.add("Omitted " + i + " duplicate stack frame" + (i == 1 ? "" : "s"));
if (i > 0) stacktrace.add(" ... " + i + " more");
}
return stacktrace;
}

private static List<String> collapseStackTrace(final StackTraceElement[] trace) {
Expand Down Expand Up @@ -164,29 +187,29 @@ private static boolean isSameClassLoader(final ClassLoader classLoader, final Cl
return loader == current;
}

private static final String IPV4_PATTERN =
"\\b(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\b";
private static final String IPV6_PATTERN =
"(?i)\\b([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}\\b|" + // Full form
"(?i)\\b([0-9a-f]{1,4}:){1,7}:\\b|" + // Trailing ::
"(?i)\\b([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}\\b|" + // :: in middle (1 group after)
"(?i)\\b([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}\\b|" + // :: in middle (2 groups after)
"(?i)\\b([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}\\b|" + // :: in middle (3 groups after)
"(?i)\\b([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}\\b|" + // :: in middle (4 groups after)
"(?i)\\b([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}\\b|" + // :: in middle (5 groups after)
"(?i)\\b[0-9a-f]{1,4}:(:[0-9a-f]{1,4}){1,6}\\b|" + // :: in middle (6 groups after)
"(?i)\\b:(:[0-9a-f]{1,4}){1,7}\\b|" + // Leading ::
"(?i)\\b::([0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4}\\b|" + // :: at start
"(?i)\\b::\\b"; // Just ::
private static final String USER_HOME_PATH_PATTERN =
"(/home/)[^/\\s]+" + // Linux: /home/username
"|(/Users/)[^/\\s]+" + // macOS: /Users/username
"|((?i)[A-Z]:\\\\Users\\\\)[^\\\\\\s]+"; // Windows: A-Z:\\Users\\username
private static final Pattern IPV4_PATTERN = Pattern.compile(
"\\b(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\b");
private static final Pattern IPV6_PATTERN = Pattern.compile(
"(?i)\\b([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}\\b|" + // Full form
"(?i)\\b([0-9a-f]{1,4}:){1,7}:\\b|" + // Trailing ::
"(?i)\\b([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}\\b|" + // :: in middle (1 group after)
"(?i)\\b([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}\\b|" + // :: in middle (2 groups after)
"(?i)\\b([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}\\b|" + // :: in middle (3 groups after)
"(?i)\\b([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}\\b|" + // :: in middle (4 groups after)
"(?i)\\b([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}\\b|" + // :: in middle (5 groups after)
"(?i)\\b[0-9a-f]{1,4}:(:[0-9a-f]{1,4}){1,6}\\b|" + // :: in middle (6 groups after)
"(?i)\\b:(:[0-9a-f]{1,4}){1,7}\\b|" + // Leading ::
"(?i)\\b::([0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4}\\b|" + // :: at start
"(?i)\\b::\\b"); // Just ::
private static final Pattern USER_HOME_PATH_PATTERN = Pattern.compile(
"(/home/)[^/\\s]+" + // Linux: /home/username
"|(/Users/)[^/\\s]+" + // macOS: /Users/username
"|((?i)[A-Z]:\\\\Users\\\\)[^\\\\\\s]+"); // Windows: A-Z:\\Users\\username

private static String anonymize(String message) {
message = message.replaceAll(IPV4_PATTERN, "[IP hidden]");
message = message.replaceAll(IPV6_PATTERN, "[IP hidden]");
message = message.replaceAll(USER_HOME_PATH_PATTERN, "$1$2$3[username hidden]");
message = IPV4_PATTERN.matcher(message).replaceAll("[IP hidden]");
message = IPV6_PATTERN.matcher(message).replaceAll("[IP hidden]");
message = USER_HOME_PATH_PATTERN.matcher(message).replaceAll("$1$2$3[username hidden]");
final var username = System.getProperty("user.name");
if (username != null) message = message.replace(username, "[username hidden]");
return message;
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=0.17.0
version=0.17.1