Skip to content

Commit 17d1b98

Browse files
authored
Merge pull request #84 from faststats-dev/errors
Improve error tracking
2 parents 0d9c757 + 9d7476e commit 17d1b98

File tree

17 files changed

+245
-26
lines changed

17 files changed

+245
-26
lines changed

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import dev.faststats.bukkit.BukkitMetrics;
44
import dev.faststats.core.ErrorTracker;
5-
import dev.faststats.core.Metrics;
65
import dev.faststats.core.chart.Chart;
76
import org.bukkit.plugin.java.JavaPlugin;
87

@@ -15,7 +14,7 @@ public class ExamplePlugin extends JavaPlugin {
1514
// context-unaware error tracker, does not automatically track errors
1615
public static final ErrorTracker CONTEXT_UNAWARE_ERROR_TRACKER = ErrorTracker.contextUnaware();
1716

18-
private final Metrics metrics = BukkitMetrics.factory()
17+
private final BukkitMetrics metrics = BukkitMetrics.factory()
1918
.url(URI.create("https://metrics.example.com/v1/collect")) // For self-hosted metrics servers only
2019

2120
// Custom example charts
@@ -36,9 +35,14 @@ public class ExamplePlugin extends JavaPlugin {
3635
.token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project
3736
.create(this);
3837

38+
@Override
39+
public void onEnable() {
40+
metrics.ready(); // register additional error handlers
41+
}
42+
3943
@Override
4044
public void onDisable() {
41-
metrics.shutdown();
45+
metrics.shutdown(); // safely shut down metrics submission
4246
}
4347

4448
public void doSomethingWrong() {

bukkit/src/main/java/dev/faststats/bukkit/BukkitMetrics.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dev.faststats.bukkit;
22

33
import dev.faststats.core.Metrics;
4+
import org.bukkit.plugin.IllegalPluginAccessException;
45
import org.bukkit.plugin.Plugin;
56
import org.jetbrains.annotations.Contract;
67

@@ -21,6 +22,18 @@ static Factory factory() {
2122
return new BukkitMetricsImpl.Factory();
2223
}
2324

25+
/**
26+
* Registers additional exception handlers on Paper-based implementations.
27+
*
28+
* @throws IllegalPluginAccessException if the plugin is not yet enabled
29+
* @apiNote This method may only be called {@link Plugin#onEnable() onEnable()}.
30+
* @since 0.14.0
31+
*/
32+
@Override
33+
void ready() throws IllegalPluginAccessException;
34+
2435
interface Factory extends Metrics.Factory<Plugin, Factory> {
36+
@Override
37+
BukkitMetrics create(Plugin object) throws IllegalStateException;
2538
}
2639
}

bukkit/src/main/java/dev/faststats/bukkit/BukkitMetricsImpl.java

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package dev.faststats.bukkit;
22

33
import com.google.gson.JsonObject;
4-
import dev.faststats.core.Metrics;
54
import dev.faststats.core.SimpleMetrics;
6-
import org.bukkit.Server;
75
import org.bukkit.plugin.Plugin;
86
import org.jetbrains.annotations.Async;
97
import org.jetbrains.annotations.Contract;
@@ -13,11 +11,9 @@
1311
import java.util.Optional;
1412
import java.util.function.Supplier;
1513
import java.util.logging.Level;
16-
import java.util.logging.Logger;
1714

1815
final class BukkitMetricsImpl extends SimpleMetrics implements BukkitMetrics {
19-
private final Logger logger;
20-
private final Server server;
16+
private final Plugin plugin;
2117

2218
private final String pluginVersion;
2319
private final String minecraftVersion;
@@ -29,8 +25,8 @@ final class BukkitMetricsImpl extends SimpleMetrics implements BukkitMetrics {
2925
private BukkitMetricsImpl(final Factory factory, final Plugin plugin, final Path config) throws IllegalStateException {
3026
super(factory, config);
3127

32-
this.logger = plugin.getLogger();
33-
this.server = plugin.getServer();
28+
this.plugin = plugin;
29+
var server = plugin.getServer();
3430

3531
this.pluginVersion = tryOrEmpty(() -> plugin.getPluginMeta().getVersion())
3632
.orElseGet(() -> plugin.getDescription().getVersion());
@@ -42,14 +38,20 @@ private BukkitMetricsImpl(final Factory factory, final Plugin plugin, final Path
4238
startSubmitting();
4339
}
4440

41+
Plugin plugin() {
42+
return plugin;
43+
}
44+
4545
private boolean checkOnlineMode() {
46+
var server = plugin.getServer();
4647
return tryOrEmpty(() -> server.getServerConfig().isProxyOnlineMode())
4748
.or(() -> tryOrEmpty(this::isProxyOnlineMode))
4849
.orElseGet(server::getOnlineMode);
4950
}
5051

5152
@SuppressWarnings("removal")
5253
private boolean isProxyOnlineMode() {
54+
var server = plugin.getServer();
5355
final var proxies = server.spigot().getPaperConfig().getConfigurationSection("proxies");
5456
if (proxies == null) return false;
5557

@@ -72,7 +74,7 @@ protected void appendDefaultData(final JsonObject charts) {
7274

7375
private int getPlayerCount() {
7476
try {
75-
return server.getOnlinePlayers().size();
77+
return plugin.getServer().getOnlinePlayers().size();
7678
} catch (final Throwable t) {
7779
error("Failed to get player count", t);
7880
return 0;
@@ -81,17 +83,26 @@ private int getPlayerCount() {
8183

8284
@Override
8385
protected void printError(final String message, @Nullable final Throwable throwable) {
84-
logger.log(Level.SEVERE, message, throwable);
86+
plugin.getLogger().log(Level.SEVERE, message, throwable);
8587
}
8688

8789
@Override
8890
protected void printInfo(final String message) {
89-
logger.info(message);
91+
plugin.getLogger().info(message);
9092
}
9193

9294
@Override
9395
protected void printWarning(final String message) {
94-
logger.warning(message);
96+
plugin.getLogger().warning(message);
97+
}
98+
99+
@Override
100+
public void ready() {
101+
if (getErrorTracker().isPresent()) try {
102+
Class.forName("com.destroystokyo.paper.event.server.ServerExceptionEvent");
103+
plugin.getServer().getPluginManager().registerEvents(new PaperEventListener(this), plugin);
104+
} catch (final ClassNotFoundException ignored) {
105+
}
95106
}
96107

97108
private <T> Optional<T> tryOrEmpty(final Supplier<T> supplier) {
@@ -104,7 +115,7 @@ private <T> Optional<T> tryOrEmpty(final Supplier<T> supplier) {
104115

105116
static final class Factory extends SimpleMetrics.Factory<Plugin, BukkitMetrics.Factory> implements BukkitMetrics.Factory {
106117
@Override
107-
public Metrics create(final Plugin plugin) throws IllegalStateException {
118+
public BukkitMetrics create(final Plugin plugin) throws IllegalStateException {
108119
final var dataFolder = getPluginsFolder(plugin).resolve("faststats");
109120
final var config = dataFolder.resolve("config.properties");
110121
return new BukkitMetricsImpl(this, plugin, config);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package dev.faststats.bukkit;
2+
3+
import com.destroystokyo.paper.event.server.ServerExceptionEvent;
4+
import com.destroystokyo.paper.exception.ServerPluginException;
5+
import org.bukkit.event.EventHandler;
6+
import org.bukkit.event.EventPriority;
7+
import org.bukkit.event.Listener;
8+
9+
record PaperEventListener(BukkitMetricsImpl metrics) implements Listener {
10+
@EventHandler(priority = EventPriority.MONITOR)
11+
public void onServerException(final ServerExceptionEvent event) {
12+
if (!(event.getException() instanceof final ServerPluginException exception)) return;
13+
if (!exception.getResponsiblePlugin().equals(metrics.plugin())) return;
14+
metrics.getErrorTracker().ifPresent(tracker -> tracker.trackError(exception));
15+
}
16+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public class ExamplePlugin extends Plugin {
3838

3939
@Override
4040
public void onDisable() {
41-
metrics.shutdown();
41+
metrics.shutdown(); // safely shut down metrics submission
4242
}
4343

4444
public void doSomethingWrong() {

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77
import java.util.ArrayList;
88
import java.util.Arrays;
9+
import java.util.Collections;
10+
import java.util.IdentityHashMap;
911
import java.util.List;
12+
import java.util.Set;
1013

1114
final class ErrorHelper {
1215
private static final int MESSAGE_LENGTH = Math.min(1000, Integer.getInteger("faststats.message-length", 500));
@@ -104,18 +107,25 @@ private static List<String> collapseConsecutiveDuplicates(final List<String> lin
104107
}
105108

106109
public static boolean isSameLoader(final ClassLoader loader, final Throwable error) {
110+
return isSameLoader(loader, error, Collections.newSetFromMap(new IdentityHashMap<>()));
111+
}
112+
113+
private static boolean isSameLoader(final ClassLoader loader, @Nullable final Throwable error, final Set<Throwable> visited) {
114+
if (error == null || !visited.add(error)) return false;
115+
107116
final var stackTrace = error.getStackTrace();
108-
if (stackTrace == null || stackTrace.length == 0) return false;
117+
if (stackTrace == null || stackTrace.length == 0)
118+
return isSameLoader(loader, error.getCause(), visited);
109119

110120
final var firstNonLibraryIndex = findFirstNonLibraryFrameIndex(stackTrace);
111-
if (firstNonLibraryIndex == -1) return false;
121+
if (firstNonLibraryIndex == -1) return isSameLoader(loader, error.getCause(), visited);
112122

113123
final var framesToCheck = Math.min(5, stackTrace.length - firstNonLibraryIndex);
114124

115125
for (var i = 0; i < framesToCheck; i++) {
116126
final var frame = stackTrace[firstNonLibraryIndex + i];
117127
if (isLibraryClass(frame.getClassName())) continue;
118-
if (!isFromLoader(frame, loader)) return false;
128+
if (!isFromLoader(frame, loader)) return isSameLoader(loader, error.getCause(), visited);
119129
}
120130

121131
return true;

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,17 @@ static ErrorTracker contextUnaware() {
163163
*/
164164
@Contract(pure = true)
165165
TrackingThreadPoolExecutor threadPoolExecutor();
166+
167+
/**
168+
* Checks if the error occurred in the same class loader as the provided loader.
169+
*
170+
* @param loader the class loader
171+
* @param error the error
172+
* @return whether the error occurred in the same class loader
173+
* @since 0.14.0
174+
*/
175+
@Contract(pure = true)
176+
static boolean isSameLoader(ClassLoader loader, Throwable error) {
177+
return ErrorHelper.isSameLoader(loader, error);
178+
}
166179
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,19 @@ public interface Metrics {
4343
Config getConfig();
4444

4545
/**
46-
* Shuts down the metrics submission.
46+
* Performs additional post-startup tasks.
47+
* <p>
48+
* This method may only be called when the application startup is complete.
49+
* <p>
50+
* <i>No-op in most implementations.</i>
51+
*
52+
* @since 0.14.0
53+
*/
54+
default void ready() {
55+
}
56+
57+
/**
58+
* Safely shuts down the metrics submission.
4759
* <p>
4860
* This method should be called when the application is shutting down.
4961
*

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public synchronized void attachErrorContext(@Nullable final ClassLoader loader)
8989
final var handler = originalHandler;
9090
if (handler != null) handler.uncaughtException(thread, error);
9191
try {
92-
if (loader != null && !ErrorHelper.isSameLoader(loader, error)) return;
92+
if (loader != null && !ErrorTracker.isSameLoader(loader, error)) return;
9393
final var event = errorEvent;
9494
if (event != null) event.accept(loader, error);
9595
trackError(error);

0 commit comments

Comments
 (0)