Skip to content

Commit a8e91e7

Browse files
committed
Extract error handling logic to ErrorHelper
1 parent 522cb69 commit a8e91e7

File tree

3 files changed

+204
-201
lines changed

3 files changed

+204
-201
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package dev.faststats.core;
2+
3+
import com.google.gson.JsonArray;
4+
import com.google.gson.JsonObject;
5+
import org.jspecify.annotations.Nullable;
6+
7+
import java.util.ArrayList;
8+
import java.util.Arrays;
9+
import java.util.List;
10+
11+
final class ErrorHelper {
12+
private static final int MESSAGE_LENGTH = Math.min(1000, Integer.getInteger("faststats.message-length", 500));
13+
private static final int STACK_TRACE_LENGTH = Math.min(500, Integer.getInteger("faststats.stack-trace-length", 300));
14+
private static final int STACK_TRACE_LIMIT = Math.min(50, Integer.getInteger("faststats.stack-trace-limit", 15));
15+
16+
public static JsonObject compile(final Throwable error, @Nullable final List<String> suppress) {
17+
final var report = new JsonObject();
18+
final var message = getAnonymizedMessage(error);
19+
20+
report.addProperty("error", error.getClass().getName());
21+
if (message != null) report.addProperty("message", message);
22+
23+
final var elements = error.getStackTrace();
24+
final var stack = collapseStackTrace(elements);
25+
final var list = new ArrayList<>(stack);
26+
if (suppress != null) list.removeAll(suppress);
27+
final var traces = Math.min(list.size(), STACK_TRACE_LIMIT);
28+
29+
final var stacktrace = populateTraces(traces, list, elements);
30+
if (!stacktrace.isEmpty()) report.add("stack", stacktrace);
31+
32+
if (error.getCause() != null) {
33+
final var toSuppress = new ArrayList<>(stack);
34+
if (suppress != null) toSuppress.addAll(suppress);
35+
report.add("cause", compile(error.getCause(), toSuppress));
36+
}
37+
38+
return report;
39+
}
40+
41+
private static JsonArray populateTraces(final int traces, final ArrayList<String> list, final StackTraceElement[] elements) {
42+
final var stacktrace = new JsonArray(traces);
43+
44+
for (var i = 0; i < traces; i++) {
45+
final var string = list.get(i);
46+
if (string.length() <= STACK_TRACE_LENGTH) stacktrace.add(string);
47+
else stacktrace.add(string.substring(0, STACK_TRACE_LENGTH) + "...");
48+
}
49+
if (traces > 0 && traces < list.size()) {
50+
stacktrace.add("and " + (list.size() - traces) + " more...");
51+
} else {
52+
final var i = elements.length - list.size();
53+
if (i > 0) stacktrace.add("Omitted " + i + " duplicate stack frame" + (i == 1 ? "" : "s"));
54+
}
55+
return stacktrace;
56+
}
57+
58+
private static List<String> collapseStackTrace(final StackTraceElement[] trace) {
59+
final var lines = Arrays.stream(trace)
60+
.map(StackTraceElement::toString)
61+
.toList();
62+
63+
return collapseRepeatingPattern(lines);
64+
}
65+
66+
private static List<String> collapseRepeatingPattern(final List<String> lines) {
67+
final var deduplicated = collapseConsecutiveDuplicates(lines);
68+
69+
final var n = deduplicated.size();
70+
71+
for (var cycleLen = 1; cycleLen <= n / 2; cycleLen++) {
72+
var isPattern = true;
73+
var repetitions = 0;
74+
75+
for (var i = 0; i < n; i++) {
76+
if (!deduplicated.get(i).equals(deduplicated.get(i % cycleLen))) {
77+
isPattern = false;
78+
break;
79+
}
80+
if (i > 0 && i % cycleLen == 0) repetitions++;
81+
}
82+
83+
if (isPattern && repetitions >= 2) {
84+
return deduplicated.subList(0, cycleLen);
85+
}
86+
}
87+
88+
return deduplicated;
89+
}
90+
91+
private static List<String> collapseConsecutiveDuplicates(final List<String> lines) {
92+
if (lines.isEmpty()) return lines;
93+
94+
final var result = new ArrayList<String>();
95+
String previous = null;
96+
97+
for (final var line : lines) {
98+
if (line.equals(previous)) continue;
99+
result.add(line);
100+
previous = line;
101+
}
102+
103+
return result;
104+
}
105+
106+
public static boolean isSameLoader(final ClassLoader loader, final Throwable error) {
107+
final var stackTrace = error.getStackTrace();
108+
if (stackTrace == null || stackTrace.length == 0) return false;
109+
110+
final var firstNonLibraryIndex = findFirstNonLibraryFrameIndex(stackTrace);
111+
if (firstNonLibraryIndex == -1) return false;
112+
113+
final var framesToCheck = Math.min(5, stackTrace.length - firstNonLibraryIndex);
114+
115+
for (var i = 0; i < framesToCheck; i++) {
116+
final var frame = stackTrace[firstNonLibraryIndex + i];
117+
if (isLibraryClass(frame.getClassName())) continue;
118+
if (!isFromLoader(frame, loader)) return false;
119+
}
120+
121+
return true;
122+
}
123+
124+
private static int findFirstNonLibraryFrameIndex(final StackTraceElement[] stackTrace) {
125+
for (var i = 0; i < stackTrace.length; i++) {
126+
if (!isLibraryClass(stackTrace[i].getClassName())) return i;
127+
}
128+
return -1;
129+
}
130+
131+
private static boolean isLibraryClass(final String className) {
132+
return className.startsWith("java.")
133+
|| className.startsWith("javax.")
134+
|| className.startsWith("sun.")
135+
|| className.startsWith("com.sun.")
136+
|| className.startsWith("jdk.");
137+
}
138+
139+
private static boolean isFromLoader(final StackTraceElement frame, final ClassLoader loader) {
140+
try {
141+
final var clazz = Class.forName(frame.getClassName(), false, loader);
142+
return isSameClassLoader(clazz.getClassLoader(), loader);
143+
} catch (final Throwable t) {
144+
return false;
145+
}
146+
}
147+
148+
private static boolean isSameClassLoader(final ClassLoader classLoader, final ClassLoader loader) {
149+
if (classLoader == loader) return true;
150+
var current = classLoader;
151+
while (current != null && current != loader) {
152+
current = current.getParent();
153+
}
154+
return loader == current;
155+
}
156+
157+
private static final String IPV4_PATTERN =
158+
"\\b(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\b";
159+
private static final String IPV6_PATTERN =
160+
"(?i)\\b([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}\\b|" + // Full form
161+
"(?i)\\b([0-9a-f]{1,4}:){1,7}:\\b|" + // Trailing ::
162+
"(?i)\\b([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}\\b|" + // :: in middle (1 group after)
163+
"(?i)\\b([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}\\b|" + // :: in middle (2 groups after)
164+
"(?i)\\b([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}\\b|" + // :: in middle (3 groups after)
165+
"(?i)\\b([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}\\b|" + // :: in middle (4 groups after)
166+
"(?i)\\b([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}\\b|" + // :: in middle (5 groups after)
167+
"(?i)\\b[0-9a-f]{1,4}:(:[0-9a-f]{1,4}){1,6}\\b|" + // :: in middle (6 groups after)
168+
"(?i)\\b:(:[0-9a-f]{1,4}){1,7}\\b|" + // Leading ::
169+
"(?i)\\b::([0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4}\\b|" + // :: at start
170+
"(?i)\\b::\\b"; // Just ::
171+
private static final String USER_HOME_PATH_PATTERN =
172+
"(/home/)[^/\\s]+" + // Linux: /home/username
173+
"|(/Users/)[^/\\s]+" + // macOS: /Users/username
174+
"|((?i)[A-Z]:\\\\Users\\\\)[^\\\\\\s]+"; // Windows: A-Z:\\Users\\username
175+
176+
private static String anonymize(String message) {
177+
message = message.replaceAll(IPV4_PATTERN, "[IP hidden]");
178+
message = message.replaceAll(IPV6_PATTERN, "[IP hidden]");
179+
message = message.replaceAll(USER_HOME_PATH_PATTERN, "$1$2$3[username hidden]");
180+
final var username = System.getProperty("user.name");
181+
if (username != null) message = message.replace(username, "[username hidden]");
182+
return message;
183+
}
184+
185+
private static @Nullable String getAnonymizedMessage(final Throwable error) {
186+
final var message = error.getMessage();
187+
if (message == null) return null;
188+
final var truncated = message.length() > MESSAGE_LENGTH
189+
? message.substring(0, MESSAGE_LENGTH) + "..."
190+
: message;
191+
return anonymize(truncated);
192+
}
193+
}

core/src/main/java/dev/faststats/core/MurmurHash3.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dev.faststats.core;
22

3+
import com.google.gson.JsonObject;
34
import org.jetbrains.annotations.Contract;
45

56
import java.nio.charset.StandardCharsets;
@@ -20,6 +21,11 @@
2021
* </p>
2122
*/
2223
final class MurmurHash3 {
24+
public static String hash(final JsonObject object) {
25+
final var hash = MurmurHash3.hash(object.toString());
26+
return Long.toHexString(hash[0]) + Long.toHexString(hash[1]);
27+
}
28+
2329
/**
2430
* Computes the 128-bit MurmurHash3 hash of the input string.
2531
* <p>
@@ -32,7 +38,7 @@ final class MurmurHash3 {
3238
* @see <a href="https://en.wikipedia.org/wiki/MurmurHash">MurmurHash on Wikipedia</a>
3339
*/
3440
@Contract(value = "_ -> new", pure = true)
35-
public static long[] hash(final String data) {
41+
private static long[] hash(final String data) {
3642
final var bytes = data.getBytes(StandardCharsets.UTF_8);
3743
var h1 = 0L;
3844
var h2 = 0L;

0 commit comments

Comments
 (0)