From 87b5ed0902e070b4d528fd12d5a812c8c7d37ee0 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 18 Jan 2026 18:04:59 +0100 Subject: [PATCH 1/6] Add error tracking system --- .../main/java/com/example/ExamplePlugin.java | 20 ++ core/build.gradle.kts | 4 +- .../main/java/dev/faststats/core/Metrics.java | 21 ++ .../dev/faststats/core/SimpleMetrics.java | 23 +- core/src/main/java/module-info.java | 1 + .../test/java/dev/faststats/MetricsTest.java | 2 +- .../test/java/dev/faststats/MockMetrics.java | 5 +- error-tracker/build.gradle.kts | 10 + .../dev/faststats/errors/ErrorTracker.java | 174 +++++++++++ .../errors/concurrent/TrackingExecutors.java | 142 +++++++++ .../concurrent/TrackingThreadFactory.java | 41 +++ .../TrackingThreadPoolExecutor.java | 82 ++++++ .../faststats/errors/impl/MurmurHash3.java | 182 ++++++++++++ .../errors/impl/SimpleErrorTracker.java | 272 ++++++++++++++++++ .../errors/impl/SimpleTrackingExecutors.java | 128 +++++++++ .../impl/SimpleTrackingThreadFactory.java | 33 +++ .../SimpleTrackingThreadPoolExecutor.java | 63 ++++ .../impl/WrappedTrackingThreadFactory.java | 26 ++ .../faststats/errors/impl/package-info.java | 4 + error-tracker/src/main/java/module-info.java | 13 + .../java/dev/faststats/ErrorTrackerTest.java | 65 +++++ gradle.properties | 2 +- settings.gradle.kts | 1 + 23 files changed, 1305 insertions(+), 9 deletions(-) create mode 100644 error-tracker/build.gradle.kts create mode 100644 error-tracker/src/main/java/dev/faststats/errors/ErrorTracker.java create mode 100644 error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingExecutors.java create mode 100644 error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingThreadFactory.java create mode 100644 error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingThreadPoolExecutor.java create mode 100644 error-tracker/src/main/java/dev/faststats/errors/impl/MurmurHash3.java create mode 100644 error-tracker/src/main/java/dev/faststats/errors/impl/SimpleErrorTracker.java create mode 100644 error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingExecutors.java create mode 100644 error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingThreadFactory.java create mode 100644 error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingThreadPoolExecutor.java create mode 100644 error-tracker/src/main/java/dev/faststats/errors/impl/WrappedTrackingThreadFactory.java create mode 100644 error-tracker/src/main/java/dev/faststats/errors/impl/package-info.java create mode 100644 error-tracker/src/main/java/module-info.java create mode 100644 error-tracker/src/test/java/dev/faststats/ErrorTrackerTest.java diff --git a/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java b/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java index 5cf2b1f..98ebd81 100644 --- a/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -3,11 +3,18 @@ import dev.faststats.bukkit.BukkitMetrics; import dev.faststats.core.Metrics; import dev.faststats.core.chart.Chart; +import dev.faststats.errors.ErrorTracker; import org.bukkit.plugin.java.JavaPlugin; import java.net.URI; public class ExamplePlugin extends JavaPlugin { + // context-aware error tracker, automatically tracks errors in the same class loader + public static final ErrorTracker ERROR_TRACKER = ErrorTracker.contextAware(); + + // context-unaware error tracker, does not automatically track errors + public static final ErrorTracker CONTEXT_UNAWARE_ERROR_TRACKER = ErrorTracker.contextUnaware(); + private final Metrics metrics = BukkitMetrics.factory() .url(URI.create("https://metrics.example.com/v1/collect")) // For self-hosted metrics servers only @@ -20,6 +27,10 @@ public class ExamplePlugin extends JavaPlugin { .addChart(Chart.numberArray("example_number_array", () -> new Number[]{1, 2, 3})) .addChart(Chart.booleanArray("example_boolean_array", () -> new Boolean[]{true, false})) + // Attach an error tracker + // This must be enabled in the project settings + .errorTracker(ERROR_TRACKER) + .debug(true) // Enable debug mode for development and testing .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project @@ -29,4 +40,13 @@ public class ExamplePlugin extends JavaPlugin { public void onDisable() { metrics.shutdown(); } + + public void doSomethingWrong() { + try { + // Do something that might throw an error + throw new RuntimeException("Something went wrong!"); + } catch (Exception e) { + CONTEXT_UNAWARE_ERROR_TRACKER.trackError(e); + } + } } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 24efd79..4ca6504 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,7 +1,5 @@ dependencies { - compileOnlyApi("com.google.code.gson:gson:2.13.2") - compileOnlyApi("org.jetbrains:annotations:26.0.2-1") - compileOnlyApi("org.jspecify:jspecify:1.0.0") + api(project(":error-tracker")) testImplementation("com.google.code.gson:gson:2.13.2") testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/core/src/main/java/dev/faststats/core/Metrics.java b/core/src/main/java/dev/faststats/core/Metrics.java index 0ec7b77..78b8b29 100644 --- a/core/src/main/java/dev/faststats/core/Metrics.java +++ b/core/src/main/java/dev/faststats/core/Metrics.java @@ -1,10 +1,12 @@ package dev.faststats.core; import dev.faststats.core.chart.Chart; +import dev.faststats.errors.ErrorTracker; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; import java.net.URI; +import java.util.Optional; import java.util.UUID; /** @@ -23,6 +25,15 @@ public interface Metrics { @Contract(pure = true) String getToken(); + /** + * Get the error tracker for this metrics instance. + * + * @return the error tracker + * @since 0.10.0 + */ + @Contract(pure = true) + Optional getErrorTracker(); + /** * Get the metrics configuration. * @@ -60,6 +71,16 @@ interface Factory { @Contract(mutates = "this") Factory addChart(Chart chart) throws IllegalArgumentException; + /** + * Sets the error tracker for this metrics instance. + * + * @param tracker the error tracker + * @return the metrics factory + * @since 0.10.0 + */ + @Contract(mutates = "this") + Factory errorTracker(ErrorTracker tracker); + /** * Enables or disabled debug mode for this metrics instance. *

diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 9f0e857..a3c2111 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -2,6 +2,7 @@ import com.google.gson.JsonObject; import dev.faststats.core.chart.Chart; +import dev.faststats.errors.ErrorTracker; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.MustBeInvokedByOverriders; @@ -52,6 +53,7 @@ public abstract class SimpleMetrics implements Metrics { private final Set> charts; private final Config config; private final @Token String token; + private final @Nullable ErrorTracker tracker; private final URI url; private final boolean debug; @@ -64,11 +66,12 @@ protected SimpleMetrics(SimpleMetrics.Factory factory, Path config) throws Il this.config = new Config(config); this.debug = factory.debug || Boolean.getBoolean("faststats.debug") || this.config.debug(); this.token = factory.token; + this.tracker = factory.tracker; this.url = factory.url; } @VisibleForTesting - protected SimpleMetrics(Config config, Set> charts, @Token String token, URI url, boolean debug) { + protected SimpleMetrics(Config config, Set> charts, @Token String token, @Nullable ErrorTracker tracker, URI url, boolean debug) { if (!token.matches(Token.PATTERN)) { throw new IllegalArgumentException("Invalid token '" + token + "', must match '" + Token.PATTERN + "'"); } @@ -77,6 +80,7 @@ protected SimpleMetrics(Config config, Set> charts, @Token String token this.config = config; this.debug = debug; this.token = token; + this.tracker = tracker; this.url = url; } @@ -174,6 +178,7 @@ protected CompletableFuture submitAsync() throws IOException { if (statusCode >= 200 && statusCode < 300) { info("Metrics submitted with status code: " + statusCode + " (" + body + ")"); + getErrorTracker().ifPresent(ErrorTracker::clear); return true; } else if (statusCode >= 300 && statusCode < 400) { warn("Received redirect response from metrics server: " + statusCode + " (" + body + ")"); @@ -219,8 +224,10 @@ protected JsonObject createData() { appendDefaultData(charts); - data.addProperty("server_id", config.serverId().toString()); + data.addProperty("identifier", config.serverId().toString()); data.add("data", charts); + + getErrorTracker().flatMap(ErrorTracker::getData).ifPresent(errors -> data.add("errors", errors)); return data; } @@ -229,6 +236,11 @@ protected JsonObject createData() { return token; } + @Override + public Optional getErrorTracker() { + return Optional.ofNullable(tracker); + } + @Override public Metrics.Config getConfig() { return config; @@ -270,6 +282,7 @@ public void shutdown() { public abstract static class Factory implements Metrics.Factory { private final Set> charts = new HashSet<>(0); private URI url = URI.create("https://metrics.faststats.dev/v1/collect"); + private @Nullable ErrorTracker tracker; private @Nullable String token; private boolean debug = false; @@ -279,6 +292,12 @@ public Metrics.Factory addChart(Chart chart) throws IllegalArgumentExcepti return this; } + @Override + public Metrics.Factory errorTracker(ErrorTracker tracker) { + this.tracker = tracker; + return this; + } + @Override public Metrics.Factory debug(boolean enabled) { this.debug = enabled; diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index fd689ee..1a16598 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -6,6 +6,7 @@ exports dev.faststats.core; requires com.google.gson; + requires dev.faststats.errors; requires java.net.http; requires static org.jetbrains.annotations; diff --git a/core/src/test/java/dev/faststats/MetricsTest.java b/core/src/test/java/dev/faststats/MetricsTest.java index 0f5ca2b..4a29017 100644 --- a/core/src/test/java/dev/faststats/MetricsTest.java +++ b/core/src/test/java/dev/faststats/MetricsTest.java @@ -10,7 +10,7 @@ public class MetricsTest { @Test public void testCreateData() throws IOException { - var mock = new MockMetrics(UUID.randomUUID(), "24f9fc423ed06194065a42d00995c600", true); + var mock = new MockMetrics(UUID.randomUUID(), "24f9fc423ed06194065a42d00995c600", null, true); assumeTrue(mock.submitAsync().join(), "For this test to run, the server must be running"); } } diff --git a/core/src/test/java/dev/faststats/MockMetrics.java b/core/src/test/java/dev/faststats/MockMetrics.java index cbc0047..8b46fbf 100644 --- a/core/src/test/java/dev/faststats/MockMetrics.java +++ b/core/src/test/java/dev/faststats/MockMetrics.java @@ -3,6 +3,7 @@ import com.google.gson.JsonObject; import dev.faststats.core.SimpleMetrics; import dev.faststats.core.Token; +import dev.faststats.errors.ErrorTracker; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -14,8 +15,8 @@ @NullMarked public class MockMetrics extends SimpleMetrics { - public MockMetrics(UUID serverId, @Token String token, boolean debug) { - super(new SimpleMetrics.Config(serverId, true, debug), Set.of(), token, URI.create("http://localhost:5000/v1/collect"), debug); + public MockMetrics(UUID serverId, @Token String token, @Nullable ErrorTracker tracker, boolean debug) { + super(new SimpleMetrics.Config(serverId, true, debug), Set.of(), token, tracker, URI.create("http://localhost:5000/v1/collect"), debug); } @Override diff --git a/error-tracker/build.gradle.kts b/error-tracker/build.gradle.kts new file mode 100644 index 0000000..0b68c3e --- /dev/null +++ b/error-tracker/build.gradle.kts @@ -0,0 +1,10 @@ +dependencies { + compileOnlyApi("com.google.code.gson:gson:2.13.2") + compileOnlyApi("org.jetbrains:annotations:26.0.2-1") + compileOnlyApi("org.jspecify:jspecify:1.0.0") + + testImplementation("com.google.code.gson:gson:2.13.2") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation(platform("org.junit:junit-bom:6.1.0-M1")) + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} \ No newline at end of file diff --git a/error-tracker/src/main/java/dev/faststats/errors/ErrorTracker.java b/error-tracker/src/main/java/dev/faststats/errors/ErrorTracker.java new file mode 100644 index 0000000..3747496 --- /dev/null +++ b/error-tracker/src/main/java/dev/faststats/errors/ErrorTracker.java @@ -0,0 +1,174 @@ +package dev.faststats.errors; + +import com.google.gson.JsonArray; +import dev.faststats.errors.concurrent.TrackingExecutors; +import dev.faststats.errors.concurrent.TrackingThreadFactory; +import dev.faststats.errors.concurrent.TrackingThreadPoolExecutor; +import dev.faststats.errors.impl.SimpleErrorTracker; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.Nullable; + +import java.security.PrivilegedAction; +import java.security.PrivilegedExceptionAction; +import java.util.Optional; + +/** + * An error tracker. + * + * @since 0.10.0 + */ +// todo: cleanup +public sealed interface ErrorTracker permits SimpleErrorTracker { + /** + * Create and attach a new context-aware error tracker. + *

+ * This tracker will automatically track errors that occur in the same class loader as the tracker itself. + *

+ * You can still manually track errors using {@code #trackError}. + * + * @return the error tracker + * @see #contextUnaware() + * @see #trackError(String) + * @see #trackError(Throwable) + * @since 0.10.0 + */ + @Contract(value = " -> new") + static ErrorTracker contextAware() { + var tracker = new SimpleErrorTracker(); + tracker.attachErrorContext(ErrorTracker.class.getClassLoader()); + return tracker; + } + + /** + * Create a new context-unaware error tracker. + *

+ * This tracker will not automatically track any errors. + *

+ * You have to manually track errors using {@code #trackError}. + * + * @return the error tracker + * @see #contextAware() + * @see #trackError(String) + * @see #trackError(Throwable) + * @since 0.10.0 + */ + @Contract(value = " -> new") + static ErrorTracker contextUnaware() { + return new SimpleErrorTracker(); + } + + /** + * Tracks an error. + * + * @param message the error message + * @see #trackError(Throwable) + * @since 0.10.0 + */ + @Contract(mutates = "this") + void trackError(String message); + + /** + * Tracks an error. + * + * @param error the error + * @since 0.10.0 + */ + @Contract(mutates = "this") + void trackError(Throwable error); + + /** + * Gets the error data. + * + * @return the error data + * @since 0.10.0 + */ + @Contract(pure = true) + Optional getData(); // todo: keep public? + + /** + * Clears the error data. + * + * @since 0.10.0 + */ + @Contract(mutates = "this") + void clear(); // todo: keep public? + + /** + * Attaches an error context to the tracker. + * + * @param loader the class loader + * @since 0.10.0 + */ + void attachErrorContext(@Nullable ClassLoader loader); + + /** + * Checks if the given error is from the given class loader. + * + * @param loader the class loader + * @param error the error + * @return {@code true} if the error is from the given class loader. + * @since 0.10.0 + */ + @Contract(pure = true) + boolean isSameLoader(ClassLoader loader, Throwable error); // todo: keep public? + + /** + * Creates a tracked runnable. + * + * @param runnable the runnable + * @return the tracked runnable + * @since 0.10.0 + */ + @Contract(value = "_ -> new", pure = true) + Runnable tracked(Runnable runnable); // todo: move to extra interface? + + /** + * Creates a tracked action. + * + * @param action the action + * @return the tracked action + * @since 0.10.0 + */ + @Contract(value = "_ -> new", pure = true) + PrivilegedAction tracked(PrivilegedAction action); // todo: move to extra interface? + + /** + * Creates a tracked exception action. + * + * @param action the exception action + * @return the tracked exception action + * @since 0.10.0 + */ + @Contract(value = "_ -> new", pure = true) + PrivilegedExceptionAction tracked(PrivilegedExceptionAction action); // todo: move to extra interface? + + /** + * Returns the tracking equivalent to {@link java.util.concurrent.Executors}. + * + * @return the tracking executors + * @see java.util.concurrent.Executors + * @since 0.10.0 + */ + @Contract(pure = true) + TrackingExecutors executors(); + + /** + * Returns the tracking equivalent to {@link java.util.concurrent.ThreadFactory}. + * + * @return the tracking thread factory + * @see java.util.concurrent.ThreadFactory + * @since 0.10.0 + */ + @Contract(pure = true) + TrackingThreadFactory threadFactory(); + + /** + * Returns the tracking equivalent to {@link java.util.concurrent.ThreadPoolExecutor}. + * + * @return the tracking thread pool executor + * @see java.util.concurrent.ThreadPoolExecutor + * @since 0.10.0 + */ + @Contract(pure = true) + TrackingThreadPoolExecutor threadPoolExecutor(); +} diff --git a/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingExecutors.java b/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingExecutors.java new file mode 100644 index 0000000..d157fca --- /dev/null +++ b/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingExecutors.java @@ -0,0 +1,142 @@ +package dev.faststats.errors.concurrent; + +import dev.faststats.errors.impl.SimpleTrackingExecutors; + +import java.security.PrivilegedAction; +import java.security.PrivilegedExceptionAction; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; + +/** + * @see java.util.concurrent.Executors + * @since 0.10.0 + */ +public sealed interface TrackingExecutors permits SimpleTrackingExecutors { + /** + * @see java.util.concurrent.Executors#newFixedThreadPool(int) + * @since 0.10.0 + */ + ExecutorService newFixedThreadPool(int threads); + + /** + * @see java.util.concurrent.Executors#newWorkStealingPool(int) + * @since 0.10.0 + */ + ExecutorService newWorkStealingPool(int parallelism); + + /** + * @see java.util.concurrent.Executors#newWorkStealingPool() + * @since 0.10.0 + */ + ExecutorService newWorkStealingPool(); + + /** + * @see java.util.concurrent.Executors#newFixedThreadPool(int, ThreadFactory) + * @since 0.10.0 + */ + ExecutorService newFixedThreadPool(int threads, ThreadFactory factory); + + /** + * @see java.util.concurrent.Executors#newSingleThreadExecutor() + * @since 0.10.0 + */ + ExecutorService newSingleThreadExecutor(); + + /** + * @see java.util.concurrent.Executors#newSingleThreadExecutor(ThreadFactory) + * @since 0.10.0 + */ + ExecutorService newSingleThreadExecutor(ThreadFactory factory); + + /** + * @see java.util.concurrent.Executors#newCachedThreadPool() + * @since 0.10.0 + */ + ExecutorService newCachedThreadPool(); + + /** + * @see java.util.concurrent.Executors#newCachedThreadPool(ThreadFactory) + * @since 0.10.0 + */ + ExecutorService newCachedThreadPool(ThreadFactory factory); + + /** + * @see java.util.concurrent.Executors#newThreadPerTaskExecutor(ThreadFactory) + * @since 0.10.0 + */ + ExecutorService newThreadPerTaskExecutor(ThreadFactory factory); + + /** + * @see java.util.concurrent.Executors#newVirtualThreadPerTaskExecutor() + * @since 0.10.0 + */ + ExecutorService newVirtualThreadPerTaskExecutor(); + + /** + * @see java.util.concurrent.Executors#newSingleThreadScheduledExecutor() + * @since 0.10.0 + */ + ScheduledExecutorService newSingleThreadScheduledExecutor(); + + /** + * @see java.util.concurrent.Executors#newSingleThreadScheduledExecutor(ThreadFactory) + * @since 0.10.0 + */ + ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory factory); + + /** + * @see java.util.concurrent.Executors#newScheduledThreadPool(int) + * @since 0.10.0 + */ + ScheduledExecutorService newScheduledThreadPool(int corePoolSize); + + /** + * @see java.util.concurrent.Executors#newScheduledThreadPool(int, ThreadFactory) + * @since 0.10.0 + */ + ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory factory); + + /** + * @see java.util.concurrent.Executors#unconfigurableExecutorService(ExecutorService) + * @since 0.10.0 + */ + ExecutorService unconfigurableExecutorService(ExecutorService executor); + + /** + * @see java.util.concurrent.Executors#unconfigurableScheduledExecutorService(ScheduledExecutorService) + * @since 0.10.0 + */ + ScheduledExecutorService unconfigurableScheduledExecutorService(ScheduledExecutorService executor); + + /** + * @see java.util.concurrent.Executors#defaultThreadFactory() + * @since 0.10.0 + */ + ThreadFactory defaultThreadFactory(); + + /** + * @see java.util.concurrent.Executors#callable(Runnable, Object) + * @since 0.10.0 + */ + Callable callable(Runnable task, T result); + + /** + * @see java.util.concurrent.Executors#callable(Runnable) + * @since 0.10.0 + */ + Callable callable(Runnable task); + + /** + * @see java.util.concurrent.Executors#callable(PrivilegedAction) + * @since 0.10.0 + */ + Callable callable(final PrivilegedAction action); + + /** + * @see java.util.concurrent.Executors#callable(PrivilegedExceptionAction) + * @since 0.10.0 + */ + Callable callable(final PrivilegedExceptionAction action); +} diff --git a/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingThreadFactory.java b/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingThreadFactory.java new file mode 100644 index 0000000..373f55d --- /dev/null +++ b/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingThreadFactory.java @@ -0,0 +1,41 @@ +package dev.faststats.errors.concurrent; + +import dev.faststats.errors.impl.SimpleTrackingThreadFactory; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NonNls; + +import java.util.concurrent.ThreadFactory; + +/** + * Creates threads that are tracked by the error tracker. + * + * @see java.util.concurrent.ThreadFactory + * @since 0.10.0 + */ +public sealed interface TrackingThreadFactory extends ThreadFactory permits SimpleTrackingThreadFactory { + /** + * Creates a new thread for the given runnable. + *

+ * The resulting thread will be tracked by the error tracker. + * + * @param runnable The runnable to execute in the new thread. + * @return The newly created thread. + * @see ThreadFactory#newThread(Runnable) + * @since 0.10.0 + */ + @Contract(value = "_ -> new", pure = true) + Thread newThread(Runnable runnable); + + /** + * Creates a new named thread for the given runnable. + *

+ * The resulting thread will be tracked by the error tracker. + * + * @param name The name of the new thread. + * @param runnable The runnable to execute in the new thread. + * @return The newly created named thread. + * @since 0.10.0 + */ + @Contract(value = "_, _ -> new", pure = true) + Thread newThread(@NonNls String name, Runnable runnable); +} diff --git a/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingThreadPoolExecutor.java b/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingThreadPoolExecutor.java new file mode 100644 index 0000000..e9d9a2a --- /dev/null +++ b/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingThreadPoolExecutor.java @@ -0,0 +1,82 @@ +package dev.faststats.errors.concurrent; + +import dev.faststats.errors.impl.SimpleTrackingThreadPoolExecutor; +import org.jetbrains.annotations.Range; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Creates thread pool executors that are tracked by the error tracker. + * + * @see java.util.concurrent.ThreadPoolExecutor + * @since 0.10.0 + */ +public sealed interface TrackingThreadPoolExecutor permits SimpleTrackingThreadPoolExecutor { + /** + * Creates a new thread pool executor. + *

+ * The resulting executor will be tracked by the error tracker. + * + * @param corePoolSize The core pool size. + * @param maximumPoolSize The maximum pool size. + * @param keepAliveTime The keep alive time. + * @param unit The time unit. + * @param workQueue The work queue. + * @return The newly created thread pool executor. + * @see ThreadPoolExecutor#ThreadPoolExecutor(int, int, long, TimeUnit, BlockingQueue) + * @since 0.10.0 + */ + ThreadPoolExecutor create(@Range(from = 0, to = Integer.MAX_VALUE) int corePoolSize, @Range(from = 0, to = Integer.MAX_VALUE) int maximumPoolSize, @Range(from = 0, to = Integer.MAX_VALUE) long keepAliveTime, TimeUnit unit, BlockingQueue workQueue); + + /** + * Creates a new thread pool executor. + * + * @param corePoolSize The core pool size. + * @param maximumPoolSize The maximum pool size. + * @param keepAliveTime The keep alive time. + * @param unit The time unit. + * @param workQueue The work queue. + * @param threadFactory The thread factory. + * @return The newly created thread pool executor. + * @see ThreadPoolExecutor#ThreadPoolExecutor(int, int, long, TimeUnit, BlockingQueue, ThreadFactory) + * @since 0.10.0 + */ + ThreadPoolExecutor create(@Range(from = 0, to = Integer.MAX_VALUE) int corePoolSize, @Range(from = 0, to = Integer.MAX_VALUE) int maximumPoolSize, @Range(from = 0, to = Integer.MAX_VALUE) long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory); + + /** + * Creates a new thread pool executor. + * + * @param corePoolSize The core pool size. + * @param maximumPoolSize The maximum pool size. + * @param keepAliveTime The keep alive time. + * @param unit The time unit. + * @param workQueue The work queue. + * @param handler The rejected execution handler. + * @return The newly created thread pool executor. + * @see ThreadPoolExecutor#ThreadPoolExecutor(int, int, long, TimeUnit, BlockingQueue, RejectedExecutionHandler) + * @since 0.10.0 + */ + ThreadPoolExecutor create(@Range(from = 0, to = Integer.MAX_VALUE) int corePoolSize, @Range(from = 0, to = Integer.MAX_VALUE) int maximumPoolSize, @Range(from = 0, to = Integer.MAX_VALUE) long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler); + + /** + * Creates a new thread pool executor. + *

+ * The resulting executor will be tracked by the error tracker. + * + * @param corePoolSize The core pool size. + * @param maximumPoolSize The maximum pool size. + * @param keepAliveTime The keep alive time. + * @param unit The time unit. + * @param workQueue The work queue. + * @param threadFactory The thread factory. + * @param handler The rejected execution handler. + * @return The newly created thread pool executor. + * @see ThreadPoolExecutor#ThreadPoolExecutor(int, int, long, TimeUnit, BlockingQueue, ThreadFactory, RejectedExecutionHandler) + * @since 0.10.0 + */ + ThreadPoolExecutor create(@Range(from = 0, to = Integer.MAX_VALUE) int corePoolSize, @Range(from = 0, to = Integer.MAX_VALUE) int maximumPoolSize, @Range(from = 0, to = Integer.MAX_VALUE) long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler); +} diff --git a/error-tracker/src/main/java/dev/faststats/errors/impl/MurmurHash3.java b/error-tracker/src/main/java/dev/faststats/errors/impl/MurmurHash3.java new file mode 100644 index 0000000..0d92a3f --- /dev/null +++ b/error-tracker/src/main/java/dev/faststats/errors/impl/MurmurHash3.java @@ -0,0 +1,182 @@ +package dev.faststats.errors.impl; + +import org.jetbrains.annotations.Contract; + +import java.nio.charset.StandardCharsets; + +/** + * Implementation of the MurmurHash3 128-bit hash algorithm. + *

+ * MurmurHash is a non-cryptographic hash function suitable for general hash-based lookup. + * It provides excellent distribution and performance while minimizing collisions. + *

+ *

+ * This implementation follows the MurmurHash3_x64_128 variant as described at: + * https://en.wikipedia.org/wiki/MurmurHash + *

+ *

+ * Original algorithm by Austin Appleby. The name comes from the two elementary operations + * it uses: multiply (MU) and rotate (R). + *

+ */ +final class MurmurHash3 { + /** + * Computes the 128-bit MurmurHash3 hash of the input string. + *

+ * The string is encoded to UTF-8 bytes before hashing. The result is returned + * as an array of two long values (64 bits each), combined they form a 128-bit hash. + *

+ * + * @param data the input string to hash + * @return a 2-element array containing the lower 64 bits at index 0 and upper 64 bits at index 1 + * @see MurmurHash on Wikipedia + */ + @Contract(value = "_ -> new", pure = true) + public static long[] hash(String data) { + var bytes = data.getBytes(StandardCharsets.UTF_8); + var h1 = 0L; + var h2 = 0L; + final var c1 = 0x87c37b91114253d5L; + final var c2 = 0x4cf5ad432745937fL; + var length = bytes.length; + var blocks = length / 16; + + // Process 128-bit blocks + for (int i = 0; i < blocks; i++) { + var k1 = getInt(bytes, i * 16); + var k2 = getInt(bytes, i * 16 + 4); + var k3 = getInt(bytes, i * 16 + 8); + var k4 = getInt(bytes, i * 16 + 12); + + k1 *= (int) c1; + k1 = Integer.rotateLeft(k1, 31); + k1 *= (int) c2; + h1 ^= k1; + + h1 = Long.rotateLeft(h1, 27); + h1 += h2; + h1 = h1 * 5 + 0x52dce729; + + k2 *= (int) c2; + k2 = Integer.rotateLeft(k2, 33); + k2 *= (int) c1; + h2 ^= k2; + + h2 = Long.rotateLeft(h2, 31); + h2 += h1; + h2 = h2 * 5 + 0x38495ab5; + } + + // Tail + var k1 = 0; + var k2 = 0; + var k3 = 0; + var k4 = 0; + var tail = blocks * 16; + + switch (length & 15) { + case 15: + k4 ^= (bytes[tail + 14] & 0xff) << 16; + case 14: + k4 ^= (bytes[tail + 13] & 0xff) << 8; + case 13: + k4 ^= (bytes[tail + 12] & 0xff); + k4 *= (int) c2; + k4 = Integer.rotateLeft(k4, 33); + k4 *= (int) c1; + h2 ^= k4; + case 12: + k3 ^= (bytes[tail + 11] & 0xff) << 24; + case 11: + k3 ^= (bytes[tail + 10] & 0xff) << 16; + case 10: + k3 ^= (bytes[tail + 9] & 0xff) << 8; + case 9: + k3 ^= (bytes[tail + 8] & 0xff); + k3 *= (int) c1; + k3 = Integer.rotateLeft(k3, 31); + k3 *= (int) c2; + h1 ^= k3; + case 8: + k2 ^= (bytes[tail + 7] & 0xff) << 24; + case 7: + k2 ^= (bytes[tail + 6] & 0xff) << 16; + case 6: + k2 ^= (bytes[tail + 5] & 0xff) << 8; + case 5: + k2 ^= (bytes[tail + 4] & 0xff); + k2 *= (int) c2; + k2 = Integer.rotateLeft(k2, 33); + k2 *= (int) c1; + h2 ^= k2; + case 4: + k1 ^= (bytes[tail + 3] & 0xff) << 24; + case 3: + k1 ^= (bytes[tail + 2] & 0xff) << 16; + case 2: + k1 ^= (bytes[tail + 1] & 0xff) << 8; + case 1: + k1 ^= (bytes[tail] & 0xff); + k1 *= (int) c1; + k1 = Integer.rotateLeft(k1, 31); + k1 *= (int) c2; + h1 ^= k1; + } + + // Finalization + h1 ^= length; + h2 ^= length; + + h1 += h2; + h2 += h1; + + h1 = fmix64(h1); + h2 = fmix64(h2); + + h1 += h2; + h2 += h1; + + return new long[]{h1, h2}; + } + + /** + * Finalization mix function to avalanche the bits in the hash. + *

+ * This function improves the distribution of the hash by XORing and multiplying + * with carefully chosen constants, ensuring that similar inputs produce very + * different outputs (avalanche effect). + *

+ * + * @param k the 64-bit value to mix + * @return the mixed 64-bit value + * @see MurmurHash Algorithm on Wikipedia + */ + @Contract(pure = true) + private static long fmix64(long k) { + k ^= k >>> 33; + k *= 0xff51afd7ed558ccdL; + k ^= k >>> 33; + k *= 0xc4ceb9fe1a85ec53L; + k ^= k >>> 33; + return k; + } + + /** + * Reads a 32-bit little-endian integer from the byte array at the specified offset. + *

+ * This helper method extracts four consecutive bytes and combines them into a + * single integer using little-endian byte order. + *

+ * + * @param bytes the byte array to read from + * @param offset the starting index in the byte array (must have at least 4 bytes from offset) + * @return the 32-bit integer value read in little-endian order + */ + @Contract(pure = true) + private static int getInt(byte[] bytes, int offset) { + return (bytes[offset] & 0xff) | + ((bytes[offset + 1] & 0xff) << 8) | + ((bytes[offset + 2] & 0xff) << 16) | + ((bytes[offset + 3] & 0xff) << 24); + } +} diff --git a/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleErrorTracker.java b/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleErrorTracker.java new file mode 100644 index 0000000..b14de4f --- /dev/null +++ b/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleErrorTracker.java @@ -0,0 +1,272 @@ +package dev.faststats.errors.impl; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import dev.faststats.errors.ErrorTracker; +import dev.faststats.errors.concurrent.TrackingExecutors; +import dev.faststats.errors.concurrent.TrackingThreadFactory; +import dev.faststats.errors.concurrent.TrackingThreadPoolExecutor; +import org.jspecify.annotations.Nullable; + +import java.security.PrivilegedAction; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public final class SimpleErrorTracker implements ErrorTracker { + private final int stackTraceLimit = Integer.getInteger("faststats.stack-trace-limit", 15); + private final Map collected = new ConcurrentHashMap<>(); + private final Map reports = new ConcurrentHashMap<>(); + + private final TrackingExecutors executors = new SimpleTrackingExecutors(this); + private final TrackingThreadFactory threadFactory = new SimpleTrackingThreadFactory(this); + private final TrackingThreadPoolExecutor threadPoolExecutor = new SimpleTrackingThreadPoolExecutor(this); + + @Override + public void trackError(String message) { + trackError(new RuntimeException(message)); + } + + @Override + public void trackError(Throwable error) { + var compile = compile(error, null); + var hashed = hash(compile); + if (collected.compute(hashed, (k, v) -> { + return v == null ? 1 : v + 1; + }) > 1) return; + reports.put(hashed, compile); + } + + private String hash(JsonObject report) { + long[] hash = MurmurHash3.hash(report.toString()); + return Long.toHexString(hash[0]) + Long.toHexString(hash[1]); + } + + private JsonObject compile(Throwable error, @Nullable List suppress) { + var stack = Arrays.asList(error.getStackTrace()); + var list = new ArrayList<>(stack); + if (suppress != null) list.removeAll(suppress); + + var traces = Math.min(list.size(), stackTraceLimit); + + var report = new JsonObject(); + var stacktrace = new JsonArray(traces); + + for (var i = 0; i < traces; i++) { + stacktrace.add(list.get(i).toString()); + } + if (traces > 0 && traces < list.size()) { + stacktrace.add("and " + (list.size() - traces) + " more..."); + } else { + var i = stack.size() - list.size(); + if (i > 0) stacktrace.add("Omitted " + i + " duplicate stack frame" + (i == 1 ? "" : "s")); + } + + report.addProperty("error", error.getClass().getName()); + if (error.getMessage() != null) { + report.addProperty("message", anonymize(error.getMessage())); + } + if (!stacktrace.isEmpty()) { + report.add("stack", stacktrace); + } + if (error.getCause() != null) { + var toSuppress = new ArrayList<>(stack); + if (suppress != null) toSuppress.addAll(suppress); + report.add("cause", compile(error.getCause(), toSuppress)); + } + + return report; + } + + 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 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]"); + var username = System.getProperty("user.name"); + if (username != null) message = message.replace(username, "[username hidden]"); + return message; + } + + @Override + public Optional getData() { + var report = new JsonArray(reports.size()); + + reports.forEach((hash, object) -> { + var copy = object.deepCopy(); + copy.addProperty("hash", hash); + var count = collected.getOrDefault(hash, 1); + if (count > 1) copy.addProperty("count", count); + report.add(copy); + }); + + collected.forEach((hash, count) -> { + if (count <= 0 || reports.containsKey(hash)) return; + var entry = new JsonObject(); + + entry.addProperty("hash", hash); + if (count > 1) entry.addProperty("count", count); + + report.add(entry); + }); + + return Optional.of(report); + } + + @Override + public void clear() { + collected.replaceAll((k, v) -> 0); + reports.clear(); + } + + @Override + public void attachErrorContext(@Nullable ClassLoader loader) { + var handler = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler((thread, error) -> { + if (handler != null) handler.uncaughtException(thread, error); + if (loader != null && !isSameLoader(loader, error)) return; + trackError(error); + }); + } + + @Override + public boolean isSameLoader(ClassLoader loader, Throwable error) { + StackTraceElement[] stackTrace = error.getStackTrace(); + if (stackTrace == null || stackTrace.length == 0) { + return false; + } + + int firstNonLibraryIndex = findFirstNonLibraryFrameIndex(stackTrace); + if (firstNonLibraryIndex == -1) { + return false; + } + + int framesToCheck = Math.min(5, stackTrace.length - firstNonLibraryIndex); + + for (int i = 0; i < framesToCheck; i++) { + StackTraceElement frame = stackTrace[firstNonLibraryIndex + i]; + if (isLibraryClass(frame.getClassName())) { + continue; + } + if (!isFromLoader(frame, loader)) { + return false; + } + } + + return true; + } + + @Override + public Runnable tracked(Runnable runnable) { + return () -> { + try { + runnable.run(); + } catch (Throwable error) { + trackError(error); + throw error; + } + }; + } + + @Override + public PrivilegedAction tracked(PrivilegedAction action) { + return () -> { + try { + return action.run(); + } catch (Throwable error) { + trackError(error); + throw error; + } + }; + } + + @Override + public PrivilegedExceptionAction tracked(PrivilegedExceptionAction action) { + return () -> { + try { + return action.run(); + } catch (Throwable error) { + trackError(error); + throw error; + } + }; + } + + @Override + public TrackingExecutors executors() { + return executors; + } + + @Override + public TrackingThreadFactory threadFactory() { + return threadFactory; + } + + @Override + public TrackingThreadPoolExecutor threadPoolExecutor() { + return threadPoolExecutor; + } + + private int findFirstNonLibraryFrameIndex(StackTraceElement[] stackTrace) { + for (int i = 0; i < stackTrace.length; i++) { + if (!isLibraryClass(stackTrace[i].getClassName())) { + return i; + } + } + return -1; + } + + private boolean isLibraryClass(String className) { + return className.startsWith("java.") + || className.startsWith("javax.") + || className.startsWith("sun.") + || className.startsWith("com.sun.") + || className.startsWith("jdk."); + } + + private boolean isFromLoader(StackTraceElement frame, ClassLoader loader) { + try { + var clazz = Class.forName(frame.getClassName(), false, loader); + return isSameClassLoader(clazz.getClassLoader(), loader); + } catch (ClassNotFoundException e) { + return false; + } + } + + private boolean isSameClassLoader(ClassLoader classLoader, ClassLoader loader) { + if (classLoader == loader) { + return true; + } + // Walk up the class loader hierarchy + ClassLoader current = classLoader; + while (current != null) { + if (current == loader) { + return true; + } + current = current.getParent(); + } + return false; + } +} diff --git a/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingExecutors.java b/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingExecutors.java new file mode 100644 index 0000000..2af53cb --- /dev/null +++ b/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingExecutors.java @@ -0,0 +1,128 @@ +package dev.faststats.errors.impl; + +import dev.faststats.errors.ErrorTracker; +import dev.faststats.errors.concurrent.TrackingExecutors; + +import java.security.PrivilegedAction; +import java.security.PrivilegedExceptionAction; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +public final class SimpleTrackingExecutors implements TrackingExecutors { + private final ErrorTracker tracker; + + public SimpleTrackingExecutors(ErrorTracker tracker) { + this.tracker = tracker; + } + + @Override + public ExecutorService newFixedThreadPool(int threads) { + return tracker.threadPoolExecutor().create(threads, threads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()); + } + + @Override + public ExecutorService newWorkStealingPool(int parallelism) { + return Executors.newWorkStealingPool(parallelism); // todo + } + + @Override + public ExecutorService newWorkStealingPool() { + return Executors.newWorkStealingPool(); // todo + } + + @Override + public ExecutorService newFixedThreadPool(int threads, ThreadFactory factory) { + return Executors.newFixedThreadPool(threads, WrappedTrackingThreadFactory.wrap(tracker, factory)); + } + + @Override + public ExecutorService newSingleThreadExecutor() { + return newSingleThreadExecutor(defaultThreadFactory()); + } + + @Override + public ExecutorService newSingleThreadExecutor(ThreadFactory factory) { + return Executors.newSingleThreadExecutor(WrappedTrackingThreadFactory.wrap(tracker, factory)); + } + + @Override + public ExecutorService newCachedThreadPool() { + return tracker.threadPoolExecutor().create(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<>()); + } + + @Override + public ExecutorService newCachedThreadPool(ThreadFactory factory) { + return Executors.newCachedThreadPool(WrappedTrackingThreadFactory.wrap(tracker, factory)); + } + + @Override + public ExecutorService newThreadPerTaskExecutor(ThreadFactory factory) { + return Executors.newSingleThreadExecutor(WrappedTrackingThreadFactory.wrap(tracker, factory)); + } + + @Override + public ExecutorService newVirtualThreadPerTaskExecutor() { + return newThreadPerTaskExecutor(Thread.ofVirtual().factory()); + } + + @Override + public ScheduledExecutorService newSingleThreadScheduledExecutor() { + return Executors.newSingleThreadScheduledExecutor(); // todo + } + + @Override + public ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory factory) { + return Executors.newSingleThreadScheduledExecutor(WrappedTrackingThreadFactory.wrap(tracker, factory)); + } + + @Override + public ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { + return Executors.newScheduledThreadPool(corePoolSize); // todo + } + + @Override + public ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory factory) { + return Executors.newScheduledThreadPool(corePoolSize, WrappedTrackingThreadFactory.wrap(tracker, factory)); + } + + @Override + public ExecutorService unconfigurableExecutorService(ExecutorService executor) { + return Executors.unconfigurableExecutorService(executor); // todo + } + + @Override + public ScheduledExecutorService unconfigurableScheduledExecutorService(ScheduledExecutorService executor) { + return Executors.unconfigurableScheduledExecutorService(executor); // todo + } + + @Override + public ThreadFactory defaultThreadFactory() { + return new SimpleTrackingThreadFactory(tracker); + } + + @Override + public Callable callable(Runnable task, T result) { + return Executors.callable(tracker.tracked(task), result); + } + + @Override + public Callable callable(Runnable task) { + return Executors.callable(tracker.tracked(task)); + } + + @Override + public Callable callable(PrivilegedAction action) { + return Executors.callable(tracker.tracked(action)); + } + + @Override + public Callable callable(PrivilegedExceptionAction action) { + return Executors.callable(tracker.tracked(action)); + } +} diff --git a/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingThreadFactory.java b/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingThreadFactory.java new file mode 100644 index 0000000..a398e25 --- /dev/null +++ b/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingThreadFactory.java @@ -0,0 +1,33 @@ +package dev.faststats.errors.impl; + +import dev.faststats.errors.ErrorTracker; +import dev.faststats.errors.concurrent.TrackingThreadFactory; + +import java.util.concurrent.atomic.AtomicInteger; + +public final class SimpleTrackingThreadFactory implements TrackingThreadFactory { + private static final AtomicInteger poolNumber = new AtomicInteger(1); + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + private final ErrorTracker tracker; + + public SimpleTrackingThreadFactory(ErrorTracker tracker) { + this.tracker = tracker; + this.group = Thread.currentThread().getThreadGroup(); + this.namePrefix = "tracking-pool-" + poolNumber.getAndIncrement() + "-thread-"; + } + + @Override + public Thread newThread(Runnable runnable) { + return newThread(namePrefix + threadNumber.getAndIncrement(), runnable); + } + + @Override + public Thread newThread(String name, Runnable runnable) { + var thread = new Thread(this.group, tracker.tracked(runnable), name, 0L); + if (thread.isDaemon()) thread.setDaemon(false); + if (thread.getPriority() != 5) thread.setPriority(5); + return thread; + } +} diff --git a/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingThreadPoolExecutor.java b/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingThreadPoolExecutor.java new file mode 100644 index 0000000..933c57b --- /dev/null +++ b/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingThreadPoolExecutor.java @@ -0,0 +1,63 @@ +package dev.faststats.errors.impl; + +import dev.faststats.errors.ErrorTracker; +import dev.faststats.errors.concurrent.TrackingThreadPoolExecutor; +import org.jetbrains.annotations.Range; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public final class SimpleTrackingThreadPoolExecutor implements TrackingThreadPoolExecutor { + private final ErrorTracker tracker; + + public SimpleTrackingThreadPoolExecutor(ErrorTracker tracker) { + this.tracker = tracker; + } + + @Override + public ThreadPoolExecutor create(@Range(from = 0, to = Integer.MAX_VALUE) int corePoolSize, @Range(from = 0, to = Integer.MAX_VALUE) int maximumPoolSize, @Range(from = 0, to = Integer.MAX_VALUE) long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) { + return new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, tracker.threadFactory()) { + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + tracker.trackError(t); + } + }; + } + + @Override + public ThreadPoolExecutor create(@Range(from = 0, to = Integer.MAX_VALUE) int corePoolSize, @Range(from = 0, to = Integer.MAX_VALUE) int maximumPoolSize, @Range(from = 0, to = Integer.MAX_VALUE) long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory) { + return new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory) { + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + tracker.trackError(t); + } + }; + } + + @Override + public ThreadPoolExecutor create(@Range(from = 0, to = Integer.MAX_VALUE) int corePoolSize, @Range(from = 0, to = Integer.MAX_VALUE) int maximumPoolSize, @Range(from = 0, to = Integer.MAX_VALUE) long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler) { + return new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler) { + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + tracker.trackError(t); + } + }; + } + + @Override + public ThreadPoolExecutor create(@Range(from = 0, to = Integer.MAX_VALUE) int corePoolSize, @Range(from = 0, to = Integer.MAX_VALUE) int maximumPoolSize, @Range(from = 0, to = Integer.MAX_VALUE) long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { + return new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler) { + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + tracker.trackError(t); + } + }; + } +} diff --git a/error-tracker/src/main/java/dev/faststats/errors/impl/WrappedTrackingThreadFactory.java b/error-tracker/src/main/java/dev/faststats/errors/impl/WrappedTrackingThreadFactory.java new file mode 100644 index 0000000..e803b5e --- /dev/null +++ b/error-tracker/src/main/java/dev/faststats/errors/impl/WrappedTrackingThreadFactory.java @@ -0,0 +1,26 @@ +package dev.faststats.errors.impl; + +import dev.faststats.errors.ErrorTracker; +import dev.faststats.errors.concurrent.TrackingThreadFactory; + +import java.util.concurrent.ThreadFactory; + +public final class WrappedTrackingThreadFactory implements ThreadFactory { + private final ErrorTracker tracker; + private final ThreadFactory factory; + + public WrappedTrackingThreadFactory(ErrorTracker tracker, ThreadFactory factory) { + this.tracker = tracker; + this.factory = factory; + } + + @Override + public Thread newThread(Runnable runnable) { + return factory.newThread(tracker.tracked(runnable)); + } + + public static ThreadFactory wrap(ErrorTracker tracker, ThreadFactory factory) { + return factory instanceof TrackingThreadFactory || factory instanceof WrappedTrackingThreadFactory + ? factory : new WrappedTrackingThreadFactory(tracker, factory); + } +} diff --git a/error-tracker/src/main/java/dev/faststats/errors/impl/package-info.java b/error-tracker/src/main/java/dev/faststats/errors/impl/package-info.java new file mode 100644 index 0000000..3cb0270 --- /dev/null +++ b/error-tracker/src/main/java/dev/faststats/errors/impl/package-info.java @@ -0,0 +1,4 @@ +@ApiStatus.Internal +package dev.faststats.errors.impl; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/error-tracker/src/main/java/module-info.java b/error-tracker/src/main/java/module-info.java new file mode 100644 index 0000000..b164539 --- /dev/null +++ b/error-tracker/src/main/java/module-info.java @@ -0,0 +1,13 @@ +import org.jspecify.annotations.NullMarked; + +@NullMarked +module dev.faststats.errors { + exports dev.faststats.errors.concurrent; + exports dev.faststats.errors; + exports dev.faststats.errors.impl; + + requires com.google.gson; + + requires static org.jetbrains.annotations; + requires static org.jspecify; +} \ No newline at end of file diff --git a/error-tracker/src/test/java/dev/faststats/ErrorTrackerTest.java b/error-tracker/src/test/java/dev/faststats/ErrorTrackerTest.java new file mode 100644 index 0000000..135f205 --- /dev/null +++ b/error-tracker/src/test/java/dev/faststats/ErrorTrackerTest.java @@ -0,0 +1,65 @@ +package dev.faststats; + +import com.google.gson.GsonBuilder; +import dev.faststats.errors.impl.SimpleErrorTracker; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.concurrent.CompletionException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ErrorTrackerTest { + // todo: add redaction tests + // todo: add nesting tests + // todo: add duplicate tests + + @Test + // todo: fix this mess + public void testCompile() throws InterruptedException { + var tracker = new SimpleErrorTracker(); + tracker.attachErrorContext(null); + tracker.trackError("Test error"); + var nestedError = new RuntimeException("Nested error"); + var error = new RuntimeException(null, nestedError); + tracker.trackError(error); + + tracker.trackError("hello my name is david"); + tracker.trackError(Path.of("").toAbsolutePath().toString()); + tracker.trackError("C:\\Users\\Luca\\AppData\\Local\\Temp\\SuckACock"); + tracker.trackError("/Users/Luca/AppData/Local/Temp/SuckACock"); + tracker.trackError("my ipv4 address is 215.223.110.131"); + tracker.trackError("my ipv6 address is f833:be65:65da:975b:4896:88f7:6964:44c0"); + + var deepAsyncError = new RuntimeException("deep async error"); + + var thisIsANiceError = new Thread(() -> { + var nestedAsyncError = new RuntimeException("nested async error", deepAsyncError); + throw new CompletionException("async error", nestedAsyncError); + }); + thisIsANiceError.start(); + thisIsANiceError.join(Duration.ofSeconds(1)); + + Thread.sleep(1000); + + tracker.trackError("Test error"); + var report = tracker.getData().orElseThrow(); + + var gson = new GsonBuilder().setPrettyPrinting().create(); + System.out.println(gson.toJson(report)); + if (true) return; + assertEquals(2, report.size()); + + var item = report.get(0).getAsJsonObject(); + var nested = report.get(1).getAsJsonObject(); + assertTrue(item.has("message")); + assertFalse(nested.has("message")); + + + assertEquals("Test error", item.get("message").getAsString()); + assertEquals(1, item.get("stack").getAsJsonArray().size()); + } +} diff --git a/gradle.properties b/gradle.properties index 68b492f..825e6cf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.9.0 +version=0.10.0 diff --git a/settings.gradle.kts b/settings.gradle.kts index 4546526..f9dc132 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,6 +7,7 @@ include("bukkit") include("bukkit:example-plugin") include("bungeecord") include("core") +include("error-tracker") include("hytale") include("hytale:example-plugin") include("minestom") From b16c432a0db55ae8dd9284603849711075b80ddc Mon Sep 17 00:00:00 2001 From: david Date: Sun, 18 Jan 2026 18:36:12 +0100 Subject: [PATCH 2/6] Remove impl package from module exports --- error-tracker/src/main/java/module-info.java | 1 - 1 file changed, 1 deletion(-) diff --git a/error-tracker/src/main/java/module-info.java b/error-tracker/src/main/java/module-info.java index b164539..114e428 100644 --- a/error-tracker/src/main/java/module-info.java +++ b/error-tracker/src/main/java/module-info.java @@ -4,7 +4,6 @@ module dev.faststats.errors { exports dev.faststats.errors.concurrent; exports dev.faststats.errors; - exports dev.faststats.errors.impl; requires com.google.gson; From b572566d7a2bd9ebf11dd0326646c2dcf7273ed9 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 18 Jan 2026 19:44:29 +0100 Subject: [PATCH 3/6] Track errors in metrics building --- .../main/java/dev/faststats/core/SimpleMetrics.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index a3c2111..e7031db 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -217,12 +217,18 @@ protected JsonObject createData() { this.charts.forEach(chart -> { try { chart.getData().ifPresent(chartData -> charts.add(chart.getId(), chartData)); - } catch (Throwable e) { - error("Failed to build chart data: " + chart.getId(), e); + } catch (Throwable t) { + error("Failed to build chart data: " + chart.getId(), t); + getErrorTracker().ifPresent(tracker -> tracker.trackError(t)); } }); - appendDefaultData(charts); + try { + appendDefaultData(charts); + } catch (Throwable t) { + error("Failed to append default data", t); + getErrorTracker().ifPresent(tracker -> tracker.trackError(t)); + } data.addProperty("identifier", config.serverId().toString()); data.add("data", charts); From e8eda0cd25c713b173be95efbda31f8b1c0bbd1b Mon Sep 17 00:00:00 2001 From: david Date: Sun, 18 Jan 2026 22:13:26 +0100 Subject: [PATCH 4/6] Use static import for StandardCharsets.UTF_8 --- .../main/java/dev/faststats/core/SimpleMetrics.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index e7031db..4d5667e 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -18,7 +18,6 @@ import java.net.http.HttpConnectTimeoutException; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; @@ -34,6 +33,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.zip.GZIPOutputStream; +import static java.nio.charset.StandardCharsets.UTF_8; + public abstract class SimpleMetrics implements Metrics { protected static final String ONBOARDING_MESSAGE = """ This plugin uses FastStats to collect anonymous usage statistics. @@ -148,7 +149,7 @@ protected boolean isSubmitting() { protected CompletableFuture submitAsync() throws IOException { var data = createData().toString(); - var bytes = data.getBytes(StandardCharsets.UTF_8); + var bytes = data.getBytes(UTF_8); info("Uncompressed data: " + data); @@ -172,7 +173,7 @@ protected CompletableFuture submitAsync() throws IOException { .build(); info("Sending metrics to: " + url); - return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)).thenApply(response -> { + return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString(UTF_8)).thenApply(response -> { var statusCode = response.statusCode(); var body = response.body(); @@ -385,7 +386,7 @@ public boolean debug() { private static Optional readOrEmpty(Path file) { if (!Files.isRegularFile(file)) return Optional.empty(); - try (var reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { + try (var reader = Files.newBufferedReader(file, UTF_8)) { var properties = new Properties(); properties.load(reader); return Optional.of(properties); @@ -397,7 +398,7 @@ private static Optional readOrEmpty(Path file) { private static void save(Path file, UUID serverId, boolean enabled, boolean debug) throws IOException { Files.createDirectories(file.getParent()); try (var out = Files.newOutputStream(file); - var writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { + var writer = new OutputStreamWriter(out, UTF_8)) { var properties = new Properties(); properties.setProperty("serverId", serverId.toString()); From 7a25f29fe93c1d6006ddd49c613dfce256bfc41e Mon Sep 17 00:00:00 2001 From: david Date: Sun, 18 Jan 2026 22:13:34 +0100 Subject: [PATCH 5/6] Add shutdown hook for graceful metrics submission --- core/src/main/java/dev/faststats/core/SimpleMetrics.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 4d5667e..4107dba 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -69,6 +69,15 @@ protected SimpleMetrics(SimpleMetrics.Factory factory, Path config) throws Il this.token = factory.token; this.tracker = factory.tracker; this.url = factory.url; + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + shutdown(); + submitAsync().join(); + } catch (Exception e) { + error("Failed to submit metrics on shutdown", e); + } + }, "metrics-shutdown-thread " + getClass().getName())); } @VisibleForTesting From a82223f828690987b66b7be0c231fb22e52423be Mon Sep 17 00:00:00 2001 From: david Date: Sun, 18 Jan 2026 22:57:20 +0100 Subject: [PATCH 6/6] Consolidate error tracker into core module --- .../main/java/com/example/ExamplePlugin.java | 2 +- core/build.gradle.kts | 4 +- .../dev/faststats/core}/ErrorTracker.java | 71 +------- .../main/java/dev/faststats/core/Metrics.java | 1 - .../java/dev/faststats/core}/MurmurHash3.java | 2 +- .../faststats/core}/SimpleErrorTracker.java | 153 +++++++----------- .../dev/faststats/core/SimpleMetrics.java | 9 +- .../faststats/core/SimpleTrackingBase.java | 50 ++++++ .../core}/SimpleTrackingExecutors.java | 15 +- .../core}/SimpleTrackingThreadFactory.java | 9 +- .../SimpleTrackingThreadPoolExecutor.java | 7 +- .../core}/WrappedTrackingThreadFactory.java | 9 +- .../core/concurrent/TrackingBase.java | 43 +++++ .../core}/concurrent/TrackingExecutors.java | 7 +- .../concurrent/TrackingThreadFactory.java | 9 +- .../TrackingThreadPoolExecutor.java | 7 +- core/src/main/java/module-info.java | 2 +- .../java/dev/faststats/ErrorTrackerTest.java | 31 +--- .../test/java/dev/faststats/MockMetrics.java | 2 +- error-tracker/build.gradle.kts | 10 -- .../faststats/errors/impl/package-info.java | 4 - error-tracker/src/main/java/module-info.java | 12 -- settings.gradle.kts | 1 - 23 files changed, 205 insertions(+), 255 deletions(-) rename {error-tracker/src/main/java/dev/faststats/errors => core/src/main/java/dev/faststats/core}/ErrorTracker.java (59%) rename {error-tracker/src/main/java/dev/faststats/errors/impl => core/src/main/java/dev/faststats/core}/MurmurHash3.java (99%) rename {error-tracker/src/main/java/dev/faststats/errors/impl => core/src/main/java/dev/faststats/core}/SimpleErrorTracker.java (62%) create mode 100644 core/src/main/java/dev/faststats/core/SimpleTrackingBase.java rename {error-tracker/src/main/java/dev/faststats/errors/impl => core/src/main/java/dev/faststats/core}/SimpleTrackingExecutors.java (89%) rename {error-tracker/src/main/java/dev/faststats/errors/impl => core/src/main/java/dev/faststats/core}/SimpleTrackingThreadFactory.java (75%) rename {error-tracker/src/main/java/dev/faststats/errors/impl => core/src/main/java/dev/faststats/core}/SimpleTrackingThreadPoolExecutor.java (92%) rename {error-tracker/src/main/java/dev/faststats/errors/impl => core/src/main/java/dev/faststats/core}/WrappedTrackingThreadFactory.java (69%) create mode 100644 core/src/main/java/dev/faststats/core/concurrent/TrackingBase.java rename {error-tracker/src/main/java/dev/faststats/errors => core/src/main/java/dev/faststats/core}/concurrent/TrackingExecutors.java (95%) rename {error-tracker/src/main/java/dev/faststats/errors => core/src/main/java/dev/faststats/core}/concurrent/TrackingThreadFactory.java (84%) rename {error-tracker/src/main/java/dev/faststats/errors => core/src/main/java/dev/faststats/core}/concurrent/TrackingThreadPoolExecutor.java (95%) rename {error-tracker => core}/src/test/java/dev/faststats/ErrorTrackerTest.java (53%) delete mode 100644 error-tracker/build.gradle.kts delete mode 100644 error-tracker/src/main/java/dev/faststats/errors/impl/package-info.java delete mode 100644 error-tracker/src/main/java/module-info.java diff --git a/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java b/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java index 98ebd81..9bdff49 100644 --- a/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java +++ b/bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -3,7 +3,7 @@ import dev.faststats.bukkit.BukkitMetrics; import dev.faststats.core.Metrics; import dev.faststats.core.chart.Chart; -import dev.faststats.errors.ErrorTracker; +import dev.faststats.core.ErrorTracker; import org.bukkit.plugin.java.JavaPlugin; import java.net.URI; diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 4ca6504..24efd79 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,5 +1,7 @@ dependencies { - api(project(":error-tracker")) + compileOnlyApi("com.google.code.gson:gson:2.13.2") + compileOnlyApi("org.jetbrains:annotations:26.0.2-1") + compileOnlyApi("org.jspecify:jspecify:1.0.0") testImplementation("com.google.code.gson:gson:2.13.2") testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/error-tracker/src/main/java/dev/faststats/errors/ErrorTracker.java b/core/src/main/java/dev/faststats/core/ErrorTracker.java similarity index 59% rename from error-tracker/src/main/java/dev/faststats/errors/ErrorTracker.java rename to core/src/main/java/dev/faststats/core/ErrorTracker.java index 3747496..4cd62c2 100644 --- a/error-tracker/src/main/java/dev/faststats/errors/ErrorTracker.java +++ b/core/src/main/java/dev/faststats/core/ErrorTracker.java @@ -1,23 +1,17 @@ -package dev.faststats.errors; +package dev.faststats.core; -import com.google.gson.JsonArray; -import dev.faststats.errors.concurrent.TrackingExecutors; -import dev.faststats.errors.concurrent.TrackingThreadFactory; -import dev.faststats.errors.concurrent.TrackingThreadPoolExecutor; -import dev.faststats.errors.impl.SimpleErrorTracker; +import dev.faststats.core.concurrent.TrackingExecutors; +import dev.faststats.core.concurrent.TrackingBase; +import dev.faststats.core.concurrent.TrackingThreadFactory; +import dev.faststats.core.concurrent.TrackingThreadPoolExecutor; import org.jetbrains.annotations.Contract; import org.jspecify.annotations.Nullable; -import java.security.PrivilegedAction; -import java.security.PrivilegedExceptionAction; -import java.util.Optional; - /** * An error tracker. * * @since 0.10.0 */ -// todo: cleanup public sealed interface ErrorTracker permits SimpleErrorTracker { /** * Create and attach a new context-aware error tracker. @@ -76,23 +70,6 @@ static ErrorTracker contextUnaware() { @Contract(mutates = "this") void trackError(Throwable error); - /** - * Gets the error data. - * - * @return the error data - * @since 0.10.0 - */ - @Contract(pure = true) - Optional getData(); // todo: keep public? - - /** - * Clears the error data. - * - * @since 0.10.0 - */ - @Contract(mutates = "this") - void clear(); // todo: keep public? - /** * Attaches an error context to the tracker. * @@ -102,45 +79,13 @@ static ErrorTracker contextUnaware() { void attachErrorContext(@Nullable ClassLoader loader); /** - * Checks if the given error is from the given class loader. + * Returns the tracking base. * - * @param loader the class loader - * @param error the error - * @return {@code true} if the error is from the given class loader. + * @return the tracking base * @since 0.10.0 */ @Contract(pure = true) - boolean isSameLoader(ClassLoader loader, Throwable error); // todo: keep public? - - /** - * Creates a tracked runnable. - * - * @param runnable the runnable - * @return the tracked runnable - * @since 0.10.0 - */ - @Contract(value = "_ -> new", pure = true) - Runnable tracked(Runnable runnable); // todo: move to extra interface? - - /** - * Creates a tracked action. - * - * @param action the action - * @return the tracked action - * @since 0.10.0 - */ - @Contract(value = "_ -> new", pure = true) - PrivilegedAction tracked(PrivilegedAction action); // todo: move to extra interface? - - /** - * Creates a tracked exception action. - * - * @param action the exception action - * @return the tracked exception action - * @since 0.10.0 - */ - @Contract(value = "_ -> new", pure = true) - PrivilegedExceptionAction tracked(PrivilegedExceptionAction action); // todo: move to extra interface? + TrackingBase base(); /** * Returns the tracking equivalent to {@link java.util.concurrent.Executors}. diff --git a/core/src/main/java/dev/faststats/core/Metrics.java b/core/src/main/java/dev/faststats/core/Metrics.java index 78b8b29..e0eb365 100644 --- a/core/src/main/java/dev/faststats/core/Metrics.java +++ b/core/src/main/java/dev/faststats/core/Metrics.java @@ -1,7 +1,6 @@ package dev.faststats.core; import dev.faststats.core.chart.Chart; -import dev.faststats.errors.ErrorTracker; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; diff --git a/error-tracker/src/main/java/dev/faststats/errors/impl/MurmurHash3.java b/core/src/main/java/dev/faststats/core/MurmurHash3.java similarity index 99% rename from error-tracker/src/main/java/dev/faststats/errors/impl/MurmurHash3.java rename to core/src/main/java/dev/faststats/core/MurmurHash3.java index 0d92a3f..b26c9bc 100644 --- a/error-tracker/src/main/java/dev/faststats/errors/impl/MurmurHash3.java +++ b/core/src/main/java/dev/faststats/core/MurmurHash3.java @@ -1,4 +1,4 @@ -package dev.faststats.errors.impl; +package dev.faststats.core; import org.jetbrains.annotations.Contract; diff --git a/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleErrorTracker.java b/core/src/main/java/dev/faststats/core/SimpleErrorTracker.java similarity index 62% rename from error-tracker/src/main/java/dev/faststats/errors/impl/SimpleErrorTracker.java rename to core/src/main/java/dev/faststats/core/SimpleErrorTracker.java index b14de4f..2349b26 100644 --- a/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleErrorTracker.java +++ b/core/src/main/java/dev/faststats/core/SimpleErrorTracker.java @@ -1,27 +1,25 @@ -package dev.faststats.errors.impl; +package dev.faststats.core; import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import dev.faststats.errors.ErrorTracker; -import dev.faststats.errors.concurrent.TrackingExecutors; -import dev.faststats.errors.concurrent.TrackingThreadFactory; -import dev.faststats.errors.concurrent.TrackingThreadPoolExecutor; +import dev.faststats.core.concurrent.TrackingBase; +import dev.faststats.core.concurrent.TrackingExecutors; +import dev.faststats.core.concurrent.TrackingThreadFactory; +import dev.faststats.core.concurrent.TrackingThreadPoolExecutor; import org.jspecify.annotations.Nullable; -import java.security.PrivilegedAction; -import java.security.PrivilegedExceptionAction; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -public final class SimpleErrorTracker implements ErrorTracker { +final class SimpleErrorTracker implements ErrorTracker { private final int stackTraceLimit = Integer.getInteger("faststats.stack-trace-limit", 15); private final Map collected = new ConcurrentHashMap<>(); private final Map reports = new ConcurrentHashMap<>(); + private final TrackingBase base = new SimpleTrackingBase(this); private final TrackingExecutors executors = new SimpleTrackingExecutors(this); private final TrackingThreadFactory threadFactory = new SimpleTrackingThreadFactory(this); private final TrackingThreadPoolExecutor threadPoolExecutor = new SimpleTrackingThreadPoolExecutor(this); @@ -86,20 +84,20 @@ private JsonObject compile(Throwable error, @Nullable List su "\\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 :: + "(?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 + "|(/Users/)[^/\\s]+" + // macOS: /Users/username + "|((?i)[A-Z]:\\\\Users\\\\)[^\\\\\\s]+"; // Windows: A-Z:\\Users\\username private String anonymize(String message) { message = message.replaceAll(IPV4_PATTERN, "[IP hidden]"); @@ -110,8 +108,7 @@ private String anonymize(String message) { return message; } - @Override - public Optional getData() { + public JsonArray getData() { var report = new JsonArray(reports.size()); reports.forEach((hash, object) -> { @@ -132,10 +129,9 @@ public Optional getData() { report.add(entry); }); - return Optional.of(report); + return report; } - @Override public void clear() { collected.replaceAll((k, v) -> 0); reports.clear(); @@ -152,21 +148,40 @@ public void attachErrorContext(@Nullable ClassLoader loader) { } @Override - public boolean isSameLoader(ClassLoader loader, Throwable error) { - StackTraceElement[] stackTrace = error.getStackTrace(); + public TrackingBase base() { + return base; + } + + @Override + public TrackingExecutors executors() { + return executors; + } + + @Override + public TrackingThreadFactory threadFactory() { + return threadFactory; + } + + @Override + public TrackingThreadPoolExecutor threadPoolExecutor() { + return threadPoolExecutor; + } + + private boolean isSameLoader(final ClassLoader loader, final Throwable error) { + var stackTrace = error.getStackTrace(); if (stackTrace == null || stackTrace.length == 0) { return false; } - int firstNonLibraryIndex = findFirstNonLibraryFrameIndex(stackTrace); + var firstNonLibraryIndex = findFirstNonLibraryFrameIndex(stackTrace); if (firstNonLibraryIndex == -1) { return false; } - int framesToCheck = Math.min(5, stackTrace.length - firstNonLibraryIndex); + var framesToCheck = Math.min(5, stackTrace.length - firstNonLibraryIndex); - for (int i = 0; i < framesToCheck; i++) { - StackTraceElement frame = stackTrace[firstNonLibraryIndex + i]; + for (var i = 0; i < framesToCheck; i++) { + var frame = stackTrace[firstNonLibraryIndex + i]; if (isLibraryClass(frame.getClassName())) { continue; } @@ -178,59 +193,8 @@ public boolean isSameLoader(ClassLoader loader, Throwable error) { return true; } - @Override - public Runnable tracked(Runnable runnable) { - return () -> { - try { - runnable.run(); - } catch (Throwable error) { - trackError(error); - throw error; - } - }; - } - - @Override - public PrivilegedAction tracked(PrivilegedAction action) { - return () -> { - try { - return action.run(); - } catch (Throwable error) { - trackError(error); - throw error; - } - }; - } - - @Override - public PrivilegedExceptionAction tracked(PrivilegedExceptionAction action) { - return () -> { - try { - return action.run(); - } catch (Throwable error) { - trackError(error); - throw error; - } - }; - } - - @Override - public TrackingExecutors executors() { - return executors; - } - - @Override - public TrackingThreadFactory threadFactory() { - return threadFactory; - } - - @Override - public TrackingThreadPoolExecutor threadPoolExecutor() { - return threadPoolExecutor; - } - - private int findFirstNonLibraryFrameIndex(StackTraceElement[] stackTrace) { - for (int i = 0; i < stackTrace.length; i++) { + private int findFirstNonLibraryFrameIndex(final StackTraceElement[] stackTrace) { + for (var i = 0; i < stackTrace.length; i++) { if (!isLibraryClass(stackTrace[i].getClassName())) { return i; } @@ -238,7 +202,7 @@ private int findFirstNonLibraryFrameIndex(StackTraceElement[] stackTrace) { return -1; } - private boolean isLibraryClass(String className) { + private boolean isLibraryClass(final String className) { return className.startsWith("java.") || className.startsWith("javax.") || className.startsWith("sun.") @@ -246,7 +210,7 @@ private boolean isLibraryClass(String className) { || className.startsWith("jdk."); } - private boolean isFromLoader(StackTraceElement frame, ClassLoader loader) { + private boolean isFromLoader(final StackTraceElement frame, final ClassLoader loader) { try { var clazz = Class.forName(frame.getClassName(), false, loader); return isSameClassLoader(clazz.getClassLoader(), loader); @@ -255,18 +219,13 @@ private boolean isFromLoader(StackTraceElement frame, ClassLoader loader) { } } - private boolean isSameClassLoader(ClassLoader classLoader, ClassLoader loader) { - if (classLoader == loader) { - return true; - } - // Walk up the class loader hierarchy - ClassLoader current = classLoader; - while (current != null) { - if (current == loader) { - return true; - } + private boolean isSameClassLoader(final ClassLoader classLoader, final ClassLoader loader) { + if (classLoader == loader) return true; + + var current = classLoader; + while (current != null && current != loader) { current = current.getParent(); } - return false; + return loader == current; } } diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 4107dba..a713133 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -2,7 +2,6 @@ import com.google.gson.JsonObject; import dev.faststats.core.chart.Chart; -import dev.faststats.errors.ErrorTracker; import org.jetbrains.annotations.Async; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.MustBeInvokedByOverriders; @@ -60,7 +59,7 @@ public abstract class SimpleMetrics implements Metrics { @Contract(mutates = "io") @SuppressWarnings("PatternValidation") - protected SimpleMetrics(SimpleMetrics.Factory factory, Path config) throws IllegalStateException { + protected SimpleMetrics(Factory factory, Path config) throws IllegalStateException { if (factory.token == null) throw new IllegalStateException("Token must be specified"); this.charts = Set.copyOf(factory.charts); @@ -188,7 +187,7 @@ protected CompletableFuture submitAsync() throws IOException { if (statusCode >= 200 && statusCode < 300) { info("Metrics submitted with status code: " + statusCode + " (" + body + ")"); - getErrorTracker().ifPresent(ErrorTracker::clear); + getErrorTracker().map(SimpleErrorTracker.class::cast).ifPresent(SimpleErrorTracker::clear); return true; } else if (statusCode >= 300 && statusCode < 400) { warn("Received redirect response from metrics server: " + statusCode + " (" + body + ")"); @@ -243,7 +242,9 @@ protected JsonObject createData() { data.addProperty("identifier", config.serverId().toString()); data.add("data", charts); - getErrorTracker().flatMap(ErrorTracker::getData).ifPresent(errors -> data.add("errors", errors)); + getErrorTracker().map(SimpleErrorTracker.class::cast) + .map(SimpleErrorTracker::getData) + .ifPresent(errors -> data.add("errors", errors)); return data; } diff --git a/core/src/main/java/dev/faststats/core/SimpleTrackingBase.java b/core/src/main/java/dev/faststats/core/SimpleTrackingBase.java new file mode 100644 index 0000000..00da8da --- /dev/null +++ b/core/src/main/java/dev/faststats/core/SimpleTrackingBase.java @@ -0,0 +1,50 @@ +package dev.faststats.core; + +import dev.faststats.core.concurrent.TrackingBase; + +import java.security.PrivilegedAction; +import java.security.PrivilegedExceptionAction; + +final class SimpleTrackingBase implements TrackingBase { + private final ErrorTracker tracker; + + public SimpleTrackingBase(ErrorTracker tracker) { + this.tracker = tracker; + } + + @Override + public Runnable tracked(Runnable runnable) { + return () -> { + try { + runnable.run(); + } catch (Throwable error) { + tracker.trackError(error); + throw error; + } + }; + } + + @Override + public PrivilegedAction tracked(PrivilegedAction action) { + return () -> { + try { + return action.run(); + } catch (Throwable error) { + tracker.trackError(error); + throw error; + } + }; + } + + @Override + public PrivilegedExceptionAction tracked(PrivilegedExceptionAction action) { + return () -> { + try { + return action.run(); + } catch (Throwable error) { + tracker.trackError(error); + throw error; + } + }; + } +} diff --git a/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingExecutors.java b/core/src/main/java/dev/faststats/core/SimpleTrackingExecutors.java similarity index 89% rename from error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingExecutors.java rename to core/src/main/java/dev/faststats/core/SimpleTrackingExecutors.java index 2af53cb..e51dc00 100644 --- a/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingExecutors.java +++ b/core/src/main/java/dev/faststats/core/SimpleTrackingExecutors.java @@ -1,7 +1,6 @@ -package dev.faststats.errors.impl; +package dev.faststats.core; -import dev.faststats.errors.ErrorTracker; -import dev.faststats.errors.concurrent.TrackingExecutors; +import dev.faststats.core.concurrent.TrackingExecutors; import java.security.PrivilegedAction; import java.security.PrivilegedExceptionAction; @@ -14,7 +13,7 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; -public final class SimpleTrackingExecutors implements TrackingExecutors { +final class SimpleTrackingExecutors implements TrackingExecutors { private final ErrorTracker tracker; public SimpleTrackingExecutors(ErrorTracker tracker) { @@ -108,21 +107,21 @@ public ThreadFactory defaultThreadFactory() { @Override public Callable callable(Runnable task, T result) { - return Executors.callable(tracker.tracked(task), result); + return Executors.callable(tracker.base().tracked(task), result); } @Override public Callable callable(Runnable task) { - return Executors.callable(tracker.tracked(task)); + return Executors.callable(tracker.base().tracked(task)); } @Override public Callable callable(PrivilegedAction action) { - return Executors.callable(tracker.tracked(action)); + return Executors.callable(tracker.base().tracked(action)); } @Override public Callable callable(PrivilegedExceptionAction action) { - return Executors.callable(tracker.tracked(action)); + return Executors.callable(tracker.base().tracked(action)); } } diff --git a/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingThreadFactory.java b/core/src/main/java/dev/faststats/core/SimpleTrackingThreadFactory.java similarity index 75% rename from error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingThreadFactory.java rename to core/src/main/java/dev/faststats/core/SimpleTrackingThreadFactory.java index a398e25..f641971 100644 --- a/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingThreadFactory.java +++ b/core/src/main/java/dev/faststats/core/SimpleTrackingThreadFactory.java @@ -1,11 +1,10 @@ -package dev.faststats.errors.impl; +package dev.faststats.core; -import dev.faststats.errors.ErrorTracker; -import dev.faststats.errors.concurrent.TrackingThreadFactory; +import dev.faststats.core.concurrent.TrackingThreadFactory; import java.util.concurrent.atomic.AtomicInteger; -public final class SimpleTrackingThreadFactory implements TrackingThreadFactory { +final class SimpleTrackingThreadFactory implements TrackingThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); @@ -25,7 +24,7 @@ public Thread newThread(Runnable runnable) { @Override public Thread newThread(String name, Runnable runnable) { - var thread = new Thread(this.group, tracker.tracked(runnable), name, 0L); + var thread = new Thread(this.group, tracker.base().tracked(runnable), name, 0L); if (thread.isDaemon()) thread.setDaemon(false); if (thread.getPriority() != 5) thread.setPriority(5); return thread; diff --git a/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingThreadPoolExecutor.java b/core/src/main/java/dev/faststats/core/SimpleTrackingThreadPoolExecutor.java similarity index 92% rename from error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingThreadPoolExecutor.java rename to core/src/main/java/dev/faststats/core/SimpleTrackingThreadPoolExecutor.java index 933c57b..89f0a3c 100644 --- a/error-tracker/src/main/java/dev/faststats/errors/impl/SimpleTrackingThreadPoolExecutor.java +++ b/core/src/main/java/dev/faststats/core/SimpleTrackingThreadPoolExecutor.java @@ -1,7 +1,6 @@ -package dev.faststats.errors.impl; +package dev.faststats.core; -import dev.faststats.errors.ErrorTracker; -import dev.faststats.errors.concurrent.TrackingThreadPoolExecutor; +import dev.faststats.core.concurrent.TrackingThreadPoolExecutor; import org.jetbrains.annotations.Range; import java.util.concurrent.BlockingQueue; @@ -10,7 +9,7 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -public final class SimpleTrackingThreadPoolExecutor implements TrackingThreadPoolExecutor { +final class SimpleTrackingThreadPoolExecutor implements TrackingThreadPoolExecutor { private final ErrorTracker tracker; public SimpleTrackingThreadPoolExecutor(ErrorTracker tracker) { diff --git a/error-tracker/src/main/java/dev/faststats/errors/impl/WrappedTrackingThreadFactory.java b/core/src/main/java/dev/faststats/core/WrappedTrackingThreadFactory.java similarity index 69% rename from error-tracker/src/main/java/dev/faststats/errors/impl/WrappedTrackingThreadFactory.java rename to core/src/main/java/dev/faststats/core/WrappedTrackingThreadFactory.java index e803b5e..469f5a3 100644 --- a/error-tracker/src/main/java/dev/faststats/errors/impl/WrappedTrackingThreadFactory.java +++ b/core/src/main/java/dev/faststats/core/WrappedTrackingThreadFactory.java @@ -1,11 +1,10 @@ -package dev.faststats.errors.impl; +package dev.faststats.core; -import dev.faststats.errors.ErrorTracker; -import dev.faststats.errors.concurrent.TrackingThreadFactory; +import dev.faststats.core.concurrent.TrackingThreadFactory; import java.util.concurrent.ThreadFactory; -public final class WrappedTrackingThreadFactory implements ThreadFactory { +final class WrappedTrackingThreadFactory implements ThreadFactory { private final ErrorTracker tracker; private final ThreadFactory factory; @@ -16,7 +15,7 @@ public WrappedTrackingThreadFactory(ErrorTracker tracker, ThreadFactory factory) @Override public Thread newThread(Runnable runnable) { - return factory.newThread(tracker.tracked(runnable)); + return factory.newThread(tracker.base().tracked(runnable)); } public static ThreadFactory wrap(ErrorTracker tracker, ThreadFactory factory) { diff --git a/core/src/main/java/dev/faststats/core/concurrent/TrackingBase.java b/core/src/main/java/dev/faststats/core/concurrent/TrackingBase.java new file mode 100644 index 0000000..e20c9c3 --- /dev/null +++ b/core/src/main/java/dev/faststats/core/concurrent/TrackingBase.java @@ -0,0 +1,43 @@ +package dev.faststats.core.concurrent; + +import org.jetbrains.annotations.Contract; + +import java.security.PrivilegedAction; +import java.security.PrivilegedExceptionAction; + +/** + * Provides tracking for various concurrency-related operations. + * + * @since 0.10.0 + */ +public interface TrackingBase { + /** + * Creates a tracked runnable. + * + * @param runnable the runnable + * @return the tracked runnable + * @since 0.10.0 + */ + @Contract(value = "_ -> new", pure = true) + Runnable tracked(Runnable runnable); + + /** + * Creates a tracked action. + * + * @param action the action + * @return the tracked action + * @since 0.10.0 + */ + @Contract(value = "_ -> new", pure = true) + PrivilegedAction tracked(PrivilegedAction action); + + /** + * Creates a tracked exception action. + * + * @param action the exception action + * @return the tracked exception action + * @since 0.10.0 + */ + @Contract(value = "_ -> new", pure = true) + PrivilegedExceptionAction tracked(PrivilegedExceptionAction action); +} diff --git a/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingExecutors.java b/core/src/main/java/dev/faststats/core/concurrent/TrackingExecutors.java similarity index 95% rename from error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingExecutors.java rename to core/src/main/java/dev/faststats/core/concurrent/TrackingExecutors.java index d157fca..d7dd437 100644 --- a/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingExecutors.java +++ b/core/src/main/java/dev/faststats/core/concurrent/TrackingExecutors.java @@ -1,6 +1,6 @@ -package dev.faststats.errors.concurrent; +package dev.faststats.core.concurrent; -import dev.faststats.errors.impl.SimpleTrackingExecutors; +import org.jetbrains.annotations.ApiStatus; import java.security.PrivilegedAction; import java.security.PrivilegedExceptionAction; @@ -13,7 +13,8 @@ * @see java.util.concurrent.Executors * @since 0.10.0 */ -public sealed interface TrackingExecutors permits SimpleTrackingExecutors { +@ApiStatus.NonExtendable +public interface TrackingExecutors { /** * @see java.util.concurrent.Executors#newFixedThreadPool(int) * @since 0.10.0 diff --git a/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingThreadFactory.java b/core/src/main/java/dev/faststats/core/concurrent/TrackingThreadFactory.java similarity index 84% rename from error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingThreadFactory.java rename to core/src/main/java/dev/faststats/core/concurrent/TrackingThreadFactory.java index 373f55d..ad0b7f7 100644 --- a/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingThreadFactory.java +++ b/core/src/main/java/dev/faststats/core/concurrent/TrackingThreadFactory.java @@ -1,6 +1,6 @@ -package dev.faststats.errors.concurrent; +package dev.faststats.core.concurrent; -import dev.faststats.errors.impl.SimpleTrackingThreadFactory; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NonNls; @@ -8,11 +8,12 @@ /** * Creates threads that are tracked by the error tracker. - * + * * @see java.util.concurrent.ThreadFactory * @since 0.10.0 */ -public sealed interface TrackingThreadFactory extends ThreadFactory permits SimpleTrackingThreadFactory { +@ApiStatus.NonExtendable +public interface TrackingThreadFactory extends ThreadFactory { /** * Creates a new thread for the given runnable. *

diff --git a/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingThreadPoolExecutor.java b/core/src/main/java/dev/faststats/core/concurrent/TrackingThreadPoolExecutor.java similarity index 95% rename from error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingThreadPoolExecutor.java rename to core/src/main/java/dev/faststats/core/concurrent/TrackingThreadPoolExecutor.java index e9d9a2a..b804037 100644 --- a/error-tracker/src/main/java/dev/faststats/errors/concurrent/TrackingThreadPoolExecutor.java +++ b/core/src/main/java/dev/faststats/core/concurrent/TrackingThreadPoolExecutor.java @@ -1,6 +1,6 @@ -package dev.faststats.errors.concurrent; +package dev.faststats.core.concurrent; -import dev.faststats.errors.impl.SimpleTrackingThreadPoolExecutor; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Range; import java.util.concurrent.BlockingQueue; @@ -15,7 +15,8 @@ * @see java.util.concurrent.ThreadPoolExecutor * @since 0.10.0 */ -public sealed interface TrackingThreadPoolExecutor permits SimpleTrackingThreadPoolExecutor { +@ApiStatus.NonExtendable +public interface TrackingThreadPoolExecutor { /** * Creates a new thread pool executor. *

diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index 1a16598..d9e56a3 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -3,10 +3,10 @@ @NullMarked module dev.faststats.core { exports dev.faststats.core.chart; + exports dev.faststats.core.concurrent; exports dev.faststats.core; requires com.google.gson; - requires dev.faststats.errors; requires java.net.http; requires static org.jetbrains.annotations; diff --git a/error-tracker/src/test/java/dev/faststats/ErrorTrackerTest.java b/core/src/test/java/dev/faststats/ErrorTrackerTest.java similarity index 53% rename from error-tracker/src/test/java/dev/faststats/ErrorTrackerTest.java rename to core/src/test/java/dev/faststats/ErrorTrackerTest.java index 135f205..0387af1 100644 --- a/error-tracker/src/test/java/dev/faststats/ErrorTrackerTest.java +++ b/core/src/test/java/dev/faststats/ErrorTrackerTest.java @@ -1,17 +1,11 @@ package dev.faststats; -import com.google.gson.GsonBuilder; -import dev.faststats.errors.impl.SimpleErrorTracker; +import dev.faststats.core.ErrorTracker; import org.junit.jupiter.api.Test; -import java.nio.file.Path; import java.time.Duration; import java.util.concurrent.CompletionException; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - public class ErrorTrackerTest { // todo: add redaction tests // todo: add nesting tests @@ -20,7 +14,7 @@ public class ErrorTrackerTest { @Test // todo: fix this mess public void testCompile() throws InterruptedException { - var tracker = new SimpleErrorTracker(); + var tracker = ErrorTracker.contextUnaware(); tracker.attachErrorContext(null); tracker.trackError("Test error"); var nestedError = new RuntimeException("Nested error"); @@ -28,9 +22,9 @@ public void testCompile() throws InterruptedException { tracker.trackError(error); tracker.trackError("hello my name is david"); - tracker.trackError(Path.of("").toAbsolutePath().toString()); - tracker.trackError("C:\\Users\\Luca\\AppData\\Local\\Temp\\SuckACock"); - tracker.trackError("/Users/Luca/AppData/Local/Temp/SuckACock"); + tracker.trackError("/home/MyName/Documents/MyFile.txt"); + tracker.trackError("C:\\Users\\MyName\\AppData\\Local\\Temp"); + tracker.trackError("/Users/MyName/AppData/Local/Temp"); tracker.trackError("my ipv4 address is 215.223.110.131"); tracker.trackError("my ipv6 address is f833:be65:65da:975b:4896:88f7:6964:44c0"); @@ -46,20 +40,5 @@ public void testCompile() throws InterruptedException { Thread.sleep(1000); tracker.trackError("Test error"); - var report = tracker.getData().orElseThrow(); - - var gson = new GsonBuilder().setPrettyPrinting().create(); - System.out.println(gson.toJson(report)); - if (true) return; - assertEquals(2, report.size()); - - var item = report.get(0).getAsJsonObject(); - var nested = report.get(1).getAsJsonObject(); - assertTrue(item.has("message")); - assertFalse(nested.has("message")); - - - assertEquals("Test error", item.get("message").getAsString()); - assertEquals(1, item.get("stack").getAsJsonArray().size()); } } diff --git a/core/src/test/java/dev/faststats/MockMetrics.java b/core/src/test/java/dev/faststats/MockMetrics.java index 8b46fbf..377b43e 100644 --- a/core/src/test/java/dev/faststats/MockMetrics.java +++ b/core/src/test/java/dev/faststats/MockMetrics.java @@ -3,7 +3,7 @@ import com.google.gson.JsonObject; import dev.faststats.core.SimpleMetrics; import dev.faststats.core.Token; -import dev.faststats.errors.ErrorTracker; +import dev.faststats.core.ErrorTracker; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; diff --git a/error-tracker/build.gradle.kts b/error-tracker/build.gradle.kts deleted file mode 100644 index 0b68c3e..0000000 --- a/error-tracker/build.gradle.kts +++ /dev/null @@ -1,10 +0,0 @@ -dependencies { - compileOnlyApi("com.google.code.gson:gson:2.13.2") - compileOnlyApi("org.jetbrains:annotations:26.0.2-1") - compileOnlyApi("org.jspecify:jspecify:1.0.0") - - testImplementation("com.google.code.gson:gson:2.13.2") - testImplementation("org.junit.jupiter:junit-jupiter") - testImplementation(platform("org.junit:junit-bom:6.1.0-M1")) - testRuntimeOnly("org.junit.platform:junit-platform-launcher") -} \ No newline at end of file diff --git a/error-tracker/src/main/java/dev/faststats/errors/impl/package-info.java b/error-tracker/src/main/java/dev/faststats/errors/impl/package-info.java deleted file mode 100644 index 3cb0270..0000000 --- a/error-tracker/src/main/java/dev/faststats/errors/impl/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -@ApiStatus.Internal -package dev.faststats.errors.impl; - -import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/error-tracker/src/main/java/module-info.java b/error-tracker/src/main/java/module-info.java deleted file mode 100644 index 114e428..0000000 --- a/error-tracker/src/main/java/module-info.java +++ /dev/null @@ -1,12 +0,0 @@ -import org.jspecify.annotations.NullMarked; - -@NullMarked -module dev.faststats.errors { - exports dev.faststats.errors.concurrent; - exports dev.faststats.errors; - - requires com.google.gson; - - requires static org.jetbrains.annotations; - requires static org.jspecify; -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f9dc132..4546526 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,7 +7,6 @@ include("bukkit") include("bukkit:example-plugin") include("bungeecord") include("core") -include("error-tracker") include("hytale") include("hytale:example-plugin") include("minestom")