Skip to content
Open
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
38 changes: 17 additions & 21 deletions core/src/main/java/dev/faststats/ErrorHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,25 @@ private static JsonObject compileAll(final TrackedError trackedError, @Nullable
@Nullable final Attributes defaultAttributes) {
final var error = trackedError.error();
final var report = new JsonObject();
final var message = anonymize(error.getMessage(), customPatterns);
final var message = anonymize(error.message(), customPatterns);

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

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

populateTraces(traces, list, elements, stacktrace);
appendCauseChain(error.getCause(), stack, suppress, stacktrace, customPatterns);
appendCauseChain(error.cause(), stack, suppress, stacktrace, customPatterns);

report.addProperty("error", error.getClass().getName());
final var first = anonymize(findFirstMessage(error, null), customPatterns);
report.addProperty("error", error.type().getName());
final var first = anonymize(findFirstMessage(error), customPatterns);
if (first != null) report.addProperty("message", first);

report.add("stack", stacktrace);
Expand All @@ -68,28 +68,26 @@ private static JsonObject compileAll(final TrackedError trackedError, @Nullable
return report;
}

// fixme: unmaintainable mess, i already forgot what it does
private static void appendCauseChain(@Nullable Throwable cause, final List<String> parentStack,
private static void appendCauseChain(TrackedError.@Nullable ThrowableSnapshot cause, final List<String> parentStack,
@Nullable final List<String> suppress, final JsonArray stacktrace,
final List<Map.Entry<Pattern, String>> customPatterns) {
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 = anonymize(cause.getMessage(), customPatterns);
while (cause != null) {
final var causeMessage = anonymize(cause.message(), customPatterns);
final var header = causeMessage != null
? "Caused by: " + cause.getClass().getName() + ": " + causeMessage
: "Caused by: " + cause.getClass().getName();
? "Caused by: " + cause.type().getName() + ": " + causeMessage
: "Caused by: " + cause.type().getName();
stacktrace.add(header);

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

cause = cause.getCause();
cause = cause.cause();
}
}

Expand Down Expand Up @@ -214,13 +212,11 @@ private static boolean isSameClassLoader(final ClassLoader classLoader, final Cl
return loader == current;
}

private static @Nullable String findFirstMessage(@Nullable final Throwable error, @Nullable Set<Throwable> visited) {
private static @Nullable String findFirstMessage(final TrackedError.@Nullable ThrowableSnapshot error) {
if (error == null) return null;
final var message = error.getMessage();
final var message = error.message();
if (message != null) return message;
if (visited == null) visited = Collections.newSetFromMap(new IdentityHashMap<>());
if (!visited.add(error)) return null;
return findFirstMessage(error.getCause(), visited);
return findFirstMessage(error.cause());
}

private static @Nullable String anonymize(@Nullable final String message, final List<Map.Entry<Pattern, String>> customPatterns) {
Expand Down
82 changes: 54 additions & 28 deletions core/src/main/java/dev/faststats/SimpleTrackedError.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.faststats;

import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.Nullable;

import java.util.Arrays;
Expand All @@ -11,14 +12,55 @@
final class SimpleTrackedError implements TrackedError {
private volatile Attributes attributes = Attributes.empty();
private volatile boolean handled = true;
private final Throwable error;
private final ThrowableSnapshot error;

SimpleTrackedError(final Throwable error) {
this.error = error;
this.error = snapshot(error, null);
}

@Contract("_, null -> !null")
private static @Nullable ThrowableSnapshot snapshot(final Throwable error, @Nullable Set<Throwable> visited) {
final var message = error.getMessage();
final var stackTrace = error.getStackTrace();
if (error.getCause() != null && visited == null)
visited = Collections.newSetFromMap(new IdentityHashMap<>());
if (visited != null && !visited.add(error)) return null;
final var cause = error.getCause() != null
? snapshot(error.getCause(), visited)
: null;
final var trace = stackTrace.length == 0 ? new Throwable().getStackTrace() : stackTrace;
return new SimpleThrowableSnapshot(error.getClass(), message, cause, trace);
}

record SimpleThrowableSnapshot(
Class<?> type,
@Nullable String message,
@Nullable ThrowableSnapshot cause,
StackTraceElement... stackTraces
) implements ThrowableSnapshot {
@Override
public StackTraceElement[] stackTraces() {
return stackTraces.clone();
}

@Override
public boolean equals(@Nullable final Object o) {
if (o == null || getClass() != o.getClass()) return false;
final SimpleThrowableSnapshot that = (SimpleThrowableSnapshot) o;
return Objects.equals(type, that.type)
&& Objects.equals(message, that.message)
&& Objects.equals(cause, that.cause)
&& Objects.deepEquals(stackTraces, that.stackTraces);
}

@Override
public int hashCode() {
return Objects.hash(type, message, cause, Arrays.hashCode(stackTraces));
}
}

@Override
public Throwable error() {
public ThrowableSnapshot error() {
return error;
}

Expand Down Expand Up @@ -50,36 +92,20 @@ public boolean equals(@Nullable final Object o) {
final SimpleTrackedError that = (SimpleTrackedError) o;
return handled == that.handled
&& Objects.equals(attributes, that.attributes)
&& deepEquals(error, that.error, Collections.newSetFromMap(new IdentityHashMap<>()));
&& Objects.equals(error, that.error);
}

@Override
public int hashCode() {
return Objects.hash(attributes, handled, hash(error, Collections.newSetFromMap(new IdentityHashMap<>())));
}

// fixme: hacky shit; it only has to compile and pass tests for now
private static boolean deepEquals(
@Nullable final Throwable first,
@Nullable final Throwable second,
final Set<Throwable> visited
) {
if (first == second) return true;
if (first == null || second == null) return false;
if (first.getClass() != second.getClass()) return false;
if (!Objects.equals(first.getMessage(), second.getMessage())) return false;
if (!Arrays.equals(first.getStackTrace(), second.getStackTrace())) return false;
if (!visited.add(first)) return true;
return deepEquals(first.getCause(), second.getCause(), visited);
return Objects.hash(attributes, handled, error);
}

private static int hash(@Nullable final Throwable error, final Set<Throwable> visited) {
if (error == null || !visited.add(error)) return 0;
return Objects.hash(
error.getClass(),
error.getMessage(),
Arrays.hashCode(error.getStackTrace()),
hash(error.getCause(), visited)
);
@Override
public String toString() {
return "SimpleTrackedError{" +
"attributes=" + attributes +
", handled=" + handled +
", error=" + error +
'}';
}
}
47 changes: 43 additions & 4 deletions core/src/main/java/dev/faststats/TrackedError.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.faststats;

import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.Nullable;

/**
* An error report with tracking metadata.
Expand All @@ -9,13 +10,51 @@
*/
public sealed interface TrackedError permits SimpleTrackedError {
/**
* Returns the tracked error.
* Returns a snapshot of the tracked error.
*
* @return the tracked error
* @since 0.24.0
* @return a snapshot of the tracked error
* @since 0.30.0
*/
@Contract(pure = true)
Throwable error();
ThrowableSnapshot error();

/**
* A snapshot of a {@link Throwable} captured when an error is tracked.
* <p>
* The snapshot preserves the throwable's type, message, cause chain, and stack trace
* without retaining the original throwable.
*
* @since 0.30.0
*/
sealed interface ThrowableSnapshot permits SimpleTrackedError.SimpleThrowableSnapshot {
/**
* Returns the throwable class.
*
* @return the throwable class
*/
Class<?> type();

/**
* Returns the throwable message.
*
* @return the throwable message, or {@code null} if none was provided
*/
@Nullable String message();

/**
* Returns a snapshot of the throwable's cause.
*
* @return a snapshot of the throwable's cause, or {@code null} if it has no cause
*/
@Nullable ThrowableSnapshot cause();

/**
* Returns a copy of the throwable's stack trace elements.
*
* @return a copy of the throwable's stack trace elements
*/
StackTraceElement[] stackTraces();
}

/**
* Returns whether the error was handled.
Expand Down
13 changes: 12 additions & 1 deletion core/src/test/java/dev/faststats/ErrorTrackerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,25 @@ public void cyclicCauseChainStopsAfterFirstVisit() {
var secondCauseCount = 0;
for (final var element : stack) {
final var line = element.getAsString();
if (line.equals("Caused by: java.lang.RuntimeException: first")) firstCauseCount++;
if (line.equals("java.lang.RuntimeException: first")) firstCauseCount++;
if (line.equals("Caused by: java.lang.IllegalStateException: second")) secondCauseCount++;
}

assertEquals(1, firstCauseCount);
assertEquals(1, secondCauseCount);
}

@Test
public void noEmptyTraces() {
final var oom = new OutOfMemoryError();
oom.setStackTrace(new StackTraceElement[0]);

tracker.trackError(oom);

final var stack = tracker.getFullData().get(0).getAsJsonObject().getAsJsonArray("stack");
assertFalse(stack.isEmpty(), "Tracked error must always have a stacktrace");
}

@Test
public void duplicateErrorsAreAggregatedWithCount() {
final var first = createStableError();
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version=0.29.4
version=0.30.0
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
Loading