Skip to content

Commit 83c5553

Browse files
authored
Merge pull request #61 from faststats-dev/feat/error-tracker
Add error tracking system
2 parents 62496cd + a82223f commit 83c5553

20 files changed

+1276
-15
lines changed

bukkit/example-plugin/src/main/java/com/example/ExamplePlugin.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@
33
import dev.faststats.bukkit.BukkitMetrics;
44
import dev.faststats.core.Metrics;
55
import dev.faststats.core.chart.Chart;
6+
import dev.faststats.core.ErrorTracker;
67
import org.bukkit.plugin.java.JavaPlugin;
78

89
import java.net.URI;
910

1011
public class ExamplePlugin extends JavaPlugin {
12+
// context-aware error tracker, automatically tracks errors in the same class loader
13+
public static final ErrorTracker ERROR_TRACKER = ErrorTracker.contextAware();
14+
15+
// context-unaware error tracker, does not automatically track errors
16+
public static final ErrorTracker CONTEXT_UNAWARE_ERROR_TRACKER = ErrorTracker.contextUnaware();
17+
1118
private final Metrics metrics = BukkitMetrics.factory()
1219
.url(URI.create("https://metrics.example.com/v1/collect")) // For self-hosted metrics servers only
1320

@@ -20,6 +27,10 @@ public class ExamplePlugin extends JavaPlugin {
2027
.addChart(Chart.numberArray("example_number_array", () -> new Number[]{1, 2, 3}))
2128
.addChart(Chart.booleanArray("example_boolean_array", () -> new Boolean[]{true, false}))
2229

30+
// Attach an error tracker
31+
// This must be enabled in the project settings
32+
.errorTracker(ERROR_TRACKER)
33+
2334
.debug(true) // Enable debug mode for development and testing
2435

2536
.token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project
@@ -29,4 +40,13 @@ public class ExamplePlugin extends JavaPlugin {
2940
public void onDisable() {
3041
metrics.shutdown();
3142
}
43+
44+
public void doSomethingWrong() {
45+
try {
46+
// Do something that might throw an error
47+
throw new RuntimeException("Something went wrong!");
48+
} catch (Exception e) {
49+
CONTEXT_UNAWARE_ERROR_TRACKER.trackError(e);
50+
}
51+
}
3252
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package dev.faststats.core;
2+
3+
import dev.faststats.core.concurrent.TrackingExecutors;
4+
import dev.faststats.core.concurrent.TrackingBase;
5+
import dev.faststats.core.concurrent.TrackingThreadFactory;
6+
import dev.faststats.core.concurrent.TrackingThreadPoolExecutor;
7+
import org.jetbrains.annotations.Contract;
8+
import org.jspecify.annotations.Nullable;
9+
10+
/**
11+
* An error tracker.
12+
*
13+
* @since 0.10.0
14+
*/
15+
public sealed interface ErrorTracker permits SimpleErrorTracker {
16+
/**
17+
* Create and attach a new context-aware error tracker.
18+
* <p>
19+
* This tracker will automatically track errors that occur in the same class loader as the tracker itself.
20+
* <p>
21+
* You can still manually track errors using {@code #trackError}.
22+
*
23+
* @return the error tracker
24+
* @see #contextUnaware()
25+
* @see #trackError(String)
26+
* @see #trackError(Throwable)
27+
* @since 0.10.0
28+
*/
29+
@Contract(value = " -> new")
30+
static ErrorTracker contextAware() {
31+
var tracker = new SimpleErrorTracker();
32+
tracker.attachErrorContext(ErrorTracker.class.getClassLoader());
33+
return tracker;
34+
}
35+
36+
/**
37+
* Create a new context-unaware error tracker.
38+
* <p>
39+
* This tracker will not automatically track any errors.
40+
* <p>
41+
* You have to manually track errors using {@code #trackError}.
42+
*
43+
* @return the error tracker
44+
* @see #contextAware()
45+
* @see #trackError(String)
46+
* @see #trackError(Throwable)
47+
* @since 0.10.0
48+
*/
49+
@Contract(value = " -> new")
50+
static ErrorTracker contextUnaware() {
51+
return new SimpleErrorTracker();
52+
}
53+
54+
/**
55+
* Tracks an error.
56+
*
57+
* @param message the error message
58+
* @see #trackError(Throwable)
59+
* @since 0.10.0
60+
*/
61+
@Contract(mutates = "this")
62+
void trackError(String message);
63+
64+
/**
65+
* Tracks an error.
66+
*
67+
* @param error the error
68+
* @since 0.10.0
69+
*/
70+
@Contract(mutates = "this")
71+
void trackError(Throwable error);
72+
73+
/**
74+
* Attaches an error context to the tracker.
75+
*
76+
* @param loader the class loader
77+
* @since 0.10.0
78+
*/
79+
void attachErrorContext(@Nullable ClassLoader loader);
80+
81+
/**
82+
* Returns the tracking base.
83+
*
84+
* @return the tracking base
85+
* @since 0.10.0
86+
*/
87+
@Contract(pure = true)
88+
TrackingBase base();
89+
90+
/**
91+
* Returns the tracking equivalent to {@link java.util.concurrent.Executors}.
92+
*
93+
* @return the tracking executors
94+
* @see java.util.concurrent.Executors
95+
* @since 0.10.0
96+
*/
97+
@Contract(pure = true)
98+
TrackingExecutors executors();
99+
100+
/**
101+
* Returns the tracking equivalent to {@link java.util.concurrent.ThreadFactory}.
102+
*
103+
* @return the tracking thread factory
104+
* @see java.util.concurrent.ThreadFactory
105+
* @since 0.10.0
106+
*/
107+
@Contract(pure = true)
108+
TrackingThreadFactory threadFactory();
109+
110+
/**
111+
* Returns the tracking equivalent to {@link java.util.concurrent.ThreadPoolExecutor}.
112+
*
113+
* @return the tracking thread pool executor
114+
* @see java.util.concurrent.ThreadPoolExecutor
115+
* @since 0.10.0
116+
*/
117+
@Contract(pure = true)
118+
TrackingThreadPoolExecutor threadPoolExecutor();
119+
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.jetbrains.annotations.Contract;
66

77
import java.net.URI;
8+
import java.util.Optional;
89
import java.util.UUID;
910

1011
/**
@@ -23,6 +24,15 @@ public interface Metrics {
2324
@Contract(pure = true)
2425
String getToken();
2526

27+
/**
28+
* Get the error tracker for this metrics instance.
29+
*
30+
* @return the error tracker
31+
* @since 0.10.0
32+
*/
33+
@Contract(pure = true)
34+
Optional<ErrorTracker> getErrorTracker();
35+
2636
/**
2737
* Get the metrics configuration.
2838
*
@@ -60,6 +70,16 @@ interface Factory<T> {
6070
@Contract(mutates = "this")
6171
Factory<T> addChart(Chart<?> chart) throws IllegalArgumentException;
6272

73+
/**
74+
* Sets the error tracker for this metrics instance.
75+
*
76+
* @param tracker the error tracker
77+
* @return the metrics factory
78+
* @since 0.10.0
79+
*/
80+
@Contract(mutates = "this")
81+
Factory<T> errorTracker(ErrorTracker tracker);
82+
6383
/**
6484
* Enables or disabled debug mode for this metrics instance.
6585
* <p>
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package dev.faststats.core;
2+
3+
import org.jetbrains.annotations.Contract;
4+
5+
import java.nio.charset.StandardCharsets;
6+
7+
/**
8+
* Implementation of the MurmurHash3 128-bit hash algorithm.
9+
* <p>
10+
* MurmurHash is a non-cryptographic hash function suitable for general hash-based lookup.
11+
* It provides excellent distribution and performance while minimizing collisions.
12+
* </p>
13+
* <p>
14+
* This implementation follows the MurmurHash3_x64_128 variant as described at:
15+
* <a href="https://en.wikipedia.org/wiki/MurmurHash">https://en.wikipedia.org/wiki/MurmurHash</a>
16+
* </p>
17+
* <p>
18+
* Original algorithm by Austin Appleby. The name comes from the two elementary operations
19+
* it uses: multiply (MU) and rotate (R).
20+
* </p>
21+
*/
22+
final class MurmurHash3 {
23+
/**
24+
* Computes the 128-bit MurmurHash3 hash of the input string.
25+
* <p>
26+
* The string is encoded to UTF-8 bytes before hashing. The result is returned
27+
* as an array of two long values (64 bits each), combined they form a 128-bit hash.
28+
* </p>
29+
*
30+
* @param data the input string to hash
31+
* @return a 2-element array containing the lower 64 bits at index 0 and upper 64 bits at index 1
32+
* @see <a href="https://en.wikipedia.org/wiki/MurmurHash">MurmurHash on Wikipedia</a>
33+
*/
34+
@Contract(value = "_ -> new", pure = true)
35+
public static long[] hash(String data) {
36+
var bytes = data.getBytes(StandardCharsets.UTF_8);
37+
var h1 = 0L;
38+
var h2 = 0L;
39+
final var c1 = 0x87c37b91114253d5L;
40+
final var c2 = 0x4cf5ad432745937fL;
41+
var length = bytes.length;
42+
var blocks = length / 16;
43+
44+
// Process 128-bit blocks
45+
for (int i = 0; i < blocks; i++) {
46+
var k1 = getInt(bytes, i * 16);
47+
var k2 = getInt(bytes, i * 16 + 4);
48+
var k3 = getInt(bytes, i * 16 + 8);
49+
var k4 = getInt(bytes, i * 16 + 12);
50+
51+
k1 *= (int) c1;
52+
k1 = Integer.rotateLeft(k1, 31);
53+
k1 *= (int) c2;
54+
h1 ^= k1;
55+
56+
h1 = Long.rotateLeft(h1, 27);
57+
h1 += h2;
58+
h1 = h1 * 5 + 0x52dce729;
59+
60+
k2 *= (int) c2;
61+
k2 = Integer.rotateLeft(k2, 33);
62+
k2 *= (int) c1;
63+
h2 ^= k2;
64+
65+
h2 = Long.rotateLeft(h2, 31);
66+
h2 += h1;
67+
h2 = h2 * 5 + 0x38495ab5;
68+
}
69+
70+
// Tail
71+
var k1 = 0;
72+
var k2 = 0;
73+
var k3 = 0;
74+
var k4 = 0;
75+
var tail = blocks * 16;
76+
77+
switch (length & 15) {
78+
case 15:
79+
k4 ^= (bytes[tail + 14] & 0xff) << 16;
80+
case 14:
81+
k4 ^= (bytes[tail + 13] & 0xff) << 8;
82+
case 13:
83+
k4 ^= (bytes[tail + 12] & 0xff);
84+
k4 *= (int) c2;
85+
k4 = Integer.rotateLeft(k4, 33);
86+
k4 *= (int) c1;
87+
h2 ^= k4;
88+
case 12:
89+
k3 ^= (bytes[tail + 11] & 0xff) << 24;
90+
case 11:
91+
k3 ^= (bytes[tail + 10] & 0xff) << 16;
92+
case 10:
93+
k3 ^= (bytes[tail + 9] & 0xff) << 8;
94+
case 9:
95+
k3 ^= (bytes[tail + 8] & 0xff);
96+
k3 *= (int) c1;
97+
k3 = Integer.rotateLeft(k3, 31);
98+
k3 *= (int) c2;
99+
h1 ^= k3;
100+
case 8:
101+
k2 ^= (bytes[tail + 7] & 0xff) << 24;
102+
case 7:
103+
k2 ^= (bytes[tail + 6] & 0xff) << 16;
104+
case 6:
105+
k2 ^= (bytes[tail + 5] & 0xff) << 8;
106+
case 5:
107+
k2 ^= (bytes[tail + 4] & 0xff);
108+
k2 *= (int) c2;
109+
k2 = Integer.rotateLeft(k2, 33);
110+
k2 *= (int) c1;
111+
h2 ^= k2;
112+
case 4:
113+
k1 ^= (bytes[tail + 3] & 0xff) << 24;
114+
case 3:
115+
k1 ^= (bytes[tail + 2] & 0xff) << 16;
116+
case 2:
117+
k1 ^= (bytes[tail + 1] & 0xff) << 8;
118+
case 1:
119+
k1 ^= (bytes[tail] & 0xff);
120+
k1 *= (int) c1;
121+
k1 = Integer.rotateLeft(k1, 31);
122+
k1 *= (int) c2;
123+
h1 ^= k1;
124+
}
125+
126+
// Finalization
127+
h1 ^= length;
128+
h2 ^= length;
129+
130+
h1 += h2;
131+
h2 += h1;
132+
133+
h1 = fmix64(h1);
134+
h2 = fmix64(h2);
135+
136+
h1 += h2;
137+
h2 += h1;
138+
139+
return new long[]{h1, h2};
140+
}
141+
142+
/**
143+
* Finalization mix function to avalanche the bits in the hash.
144+
* <p>
145+
* This function improves the distribution of the hash by XORing and multiplying
146+
* with carefully chosen constants, ensuring that similar inputs produce very
147+
* different outputs (avalanche effect).
148+
* </p>
149+
*
150+
* @param k the 64-bit value to mix
151+
* @return the mixed 64-bit value
152+
* @see <a href="https://en.wikipedia.org/wiki/MurmurHash#Algorithm">MurmurHash Algorithm on Wikipedia</a>
153+
*/
154+
@Contract(pure = true)
155+
private static long fmix64(long k) {
156+
k ^= k >>> 33;
157+
k *= 0xff51afd7ed558ccdL;
158+
k ^= k >>> 33;
159+
k *= 0xc4ceb9fe1a85ec53L;
160+
k ^= k >>> 33;
161+
return k;
162+
}
163+
164+
/**
165+
* Reads a 32-bit little-endian integer from the byte array at the specified offset.
166+
* <p>
167+
* This helper method extracts four consecutive bytes and combines them into a
168+
* single integer using little-endian byte order.
169+
* </p>
170+
*
171+
* @param bytes the byte array to read from
172+
* @param offset the starting index in the byte array (must have at least 4 bytes from offset)
173+
* @return the 32-bit integer value read in little-endian order
174+
*/
175+
@Contract(pure = true)
176+
private static int getInt(byte[] bytes, int offset) {
177+
return (bytes[offset] & 0xff) |
178+
((bytes[offset + 1] & 0xff) << 8) |
179+
((bytes[offset + 2] & 0xff) << 16) |
180+
((bytes[offset + 3] & 0xff) << 24);
181+
}
182+
}

0 commit comments

Comments
 (0)