From 361960554aadd74b70a27cdba026f209f2786c31 Mon Sep 17 00:00:00 2001
From: PikaMug <2267126+PikaMug@users.noreply.github.com>
Date: Wed, 17 Feb 2021 23:27:09 -0500
Subject: [PATCH] Enable bStats metrics. Bump version
---
dist/pom.xml | 2 +-
main/pom.xml | 2 +-
.../main/java/me/blackvein/quests/Quests.java | 5 +-
.../blackvein/quests/statistics/Metrics.java | 1365 +++++++++--------
pom.xml | 4 +-
v1_8_R1/pom.xml | 2 +-
v1_8_R2/pom.xml | 2 +-
v1_8_R3/pom.xml | 2 +-
8 files changed, 747 insertions(+), 637 deletions(-)
diff --git a/dist/pom.xml b/dist/pom.xml
index 01e349ba9..eb53af2f3 100644
--- a/dist/pom.xml
+++ b/dist/pom.xml
@@ -5,7 +5,7 @@
me.blackvein.quests
quests-parent
- 4.0.0-rc.4
+ 4.0.0
quests-dist
pom
diff --git a/main/pom.xml b/main/pom.xml
index 79187e3f4..f23b684c9 100644
--- a/main/pom.xml
+++ b/main/pom.xml
@@ -4,7 +4,7 @@
me.blackvein.quests
quests-parent
- 4.0.0-rc.4
+ 4.0.0
quests-main
diff --git a/main/src/main/java/me/blackvein/quests/Quests.java b/main/src/main/java/me/blackvein/quests/Quests.java
index b009c7f79..ddf518a66 100644
--- a/main/src/main/java/me/blackvein/quests/Quests.java
+++ b/main/src/main/java/me/blackvein/quests/Quests.java
@@ -95,6 +95,7 @@ import me.blackvein.quests.listeners.ItemListener;
import me.blackvein.quests.listeners.NpcListener;
import me.blackvein.quests.listeners.PartiesListener;
import me.blackvein.quests.listeners.PlayerListener;
+import me.blackvein.quests.statistics.Metrics;
import me.blackvein.quests.storage.Storage;
import me.blackvein.quests.storage.StorageFactory;
import me.blackvein.quests.tasks.NpcEffectThread;
@@ -162,8 +163,8 @@ public class Quests extends JavaPlugin implements ConversationAbandonedListener
conditionFactory = new ConditionFactory(this);
depends = new Dependencies(this);
trigger = new DenizenTrigger(this);
- /*final Metrics metrics = new Metrics(this);
- metrics.addCustomChart(new Metrics.SimplePie("language", () -> Lang.getISO()));*/
+ final Metrics metrics = new Metrics(this);
+ metrics.addCustomChart(new Metrics.SimplePie("language", () -> Lang.getISO()));
// 2 - Load main config
settings.init();
diff --git a/main/src/main/java/me/blackvein/quests/statistics/Metrics.java b/main/src/main/java/me/blackvein/quests/statistics/Metrics.java
index fe9c7933b..cb06f69f6 100644
--- a/main/src/main/java/me/blackvein/quests/statistics/Metrics.java
+++ b/main/src/main/java/me/blackvein/quests/statistics/Metrics.java
@@ -18,21 +18,25 @@ import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
-import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
-import java.util.List;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
import java.util.logging.Level;
+import java.util.stream.Collectors;
import java.util.zip.GZIPOutputStream;
import javax.net.ssl.HttpsURLConnection;
@@ -41,717 +45,822 @@ import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
-import org.bukkit.plugin.RegisteredServiceProvider;
-import org.bukkit.plugin.ServicePriority;
+import org.bukkit.plugin.java.JavaPlugin;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import com.google.gson.JsonPrimitive;
-
-/**
- * bStats collects some data for plugin authors.
- *
- * Check out https://bStats.org/ to learn more about bStats!
- */
-//@SuppressWarnings({"WeakerAccess", "unused"})
public class Metrics {
- static {
- // You can use the property to disable the check in your test environment
- if (System.getProperty("bstats.relocatecheck") == null || !System.getProperty("bstats.relocatecheck").equals("false")) {
- // Maven's Relocate is clever and changes strings, too. So we have to use this little "trick" ... :D
- final String defaultPackage = new String(
- new byte[]{'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's', '.', 'b', 'u', 'k', 'k', 'i', 't'});
- final String examplePackage = new String(new byte[]{'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'});
- // We want to make sure nobody just copy & pastes the example and use the wrong package names
- if (Metrics.class.getPackage().getName().equals(defaultPackage) || Metrics.class.getPackage().getName().equals(examplePackage)) {
- throw new IllegalStateException("bStats Metrics class has not been relocated correctly!");
- }
- }
+ private final Plugin plugin;
+
+ private final MetricsBase metricsBase;
+
+ /**
+ * Creates a new Metrics instance.
+ *
+ * @param plugin Your plugin instance.
+ * @param serviceId The id of the service. It can be found at What is my plugin id?
+ */
+ public Metrics(final JavaPlugin plugin) {
+ this.plugin = plugin;
+ final int serviceId = 9528;
+ // Get the config file
+ final File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats");
+ final File configFile = new File(bStatsFolder, "config.yml");
+ final YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
+ if (!config.isSet("serverUuid")) {
+ config.addDefault("enabled", true);
+ config.addDefault("serverUuid", UUID.randomUUID().toString());
+ config.addDefault("logFailedRequests", false);
+ config.addDefault("logSentData", false);
+ config.addDefault("logResponseStatusText", false);
+ // Inform the server owners about bStats
+ config
+ .options()
+ .header(
+ "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n"
+ + "many people use their plugin and their total player count. It's recommended to keep bStats\n"
+ + "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n"
+ + "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n"
+ + "anonymous.")
+ .copyDefaults(true);
+ try {
+ config.save(configFile);
+ } catch (final IOException ignored) {
+ }
}
+ // Load the data
+ final boolean enabled = config.getBoolean("enabled", true);
+ final String serverUUID = config.getString("serverUuid");
+ final boolean logErrors = config.getBoolean("logFailedRequests", false);
+ final boolean logSentData = config.getBoolean("logSentData", false);
+ final boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false);
+ metricsBase =
+ new MetricsBase(
+ "bukkit",
+ serverUUID,
+ serviceId,
+ enabled,
+ this::appendPlatformData,
+ this::appendServiceData,
+ submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask),
+ plugin::isEnabled,
+ (message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error),
+ (message) -> this.plugin.getLogger().log(Level.INFO, message),
+ logErrors,
+ logSentData,
+ logResponseStatusText);
+ }
- // This ThreadFactory enforces the naming convention for our Threads
- private final ThreadFactory threadFactory = task -> new Thread(task, "bStats-Metrics");
+ /**
+ * Adds a custom chart.
+ *
+ * @param chart The chart to add.
+ */
+ public void addCustomChart(final CustomChart chart) {
+ metricsBase.addCustomChart(chart);
+ }
- // Executor service for requests
- // We use an executor service because the Bukkit scheduler is affected by server lags
- private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, threadFactory);
+ private void appendPlatformData(final JsonObjectBuilder builder) {
+ builder.appendField("playerAmount", getPlayerAmount());
+ builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0);
+ builder.appendField("bukkitVersion", Bukkit.getVersion());
+ builder.appendField("bukkitName", Bukkit.getName());
+ builder.appendField("javaVersion", System.getProperty("java.version"));
+ builder.appendField("osName", System.getProperty("os.name"));
+ builder.appendField("osArch", System.getProperty("os.arch"));
+ builder.appendField("osVersion", System.getProperty("os.version"));
+ builder.appendField("coreCount", Runtime.getRuntime().availableProcessors());
+ }
- // The version of this bStats class
- public static final int B_STATS_VERSION = 1;
+ private void appendServiceData(final JsonObjectBuilder builder) {
+ final String pluginVersion = plugin.getDescription().getVersion();
+ final String adjustedVersion = pluginVersion.contains("-")
+ ? pluginVersion.substring(0, pluginVersion.indexOf("-")) : pluginVersion;
+ builder.appendField("pluginVersion", adjustedVersion);
+ }
- // The url to which the data is sent
- private static final String URL = "https://bStats.org/submitData/bukkit";
+ private int getPlayerAmount() {
+ try {
+ // Around MC 1.8 the return type was changed from an array to a collection,
+ // This fixes java.lang.NoSuchMethodError:
+ // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection;
+ final Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers");
+ return onlinePlayersMethod.getReturnType().equals(Collection.class)
+ ? ((Collection>) onlinePlayersMethod.invoke(Bukkit.getServer())).size()
+ : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length;
+ } catch (final Exception e) {
+ // Just use the new method if the reflection failed
+ return Bukkit.getOnlinePlayers().size();
+ }
+ }
+
+ public static class MetricsBase {
+
+ /** The version of the Metrics class. */
+ public static final String METRICS_VERSION = "2.2.1";
+
+ private static final ScheduledExecutorService scheduler =
+ Executors.newScheduledThreadPool(1, task -> new Thread(task, "bStats-Metrics"));
+
+ private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s";
+
+ private final String platform;
+
+ private final String serverUuid;
+
+ private final int serviceId;
+
+ private final Consumer appendPlatformDataConsumer;
+
+ private final Consumer appendServiceDataConsumer;
+
+ private final Consumer submitTaskConsumer;
+
+ private final Supplier checkServiceEnabledSupplier;
+
+ private final BiConsumer errorLogger;
+
+ private final Consumer infoLogger;
+
+ private final boolean logErrors;
+
+ private final boolean logSentData;
+
+ private final boolean logResponseStatusText;
+
+ private final Set customCharts = new HashSet<>();
- // Is bStats enabled on this server?
private final boolean enabled;
- // Should failed requests be logged?
- private static boolean logFailedRequests;
+ /**
+ * Creates a new MetricsBase class instance.
+ *
+ * @param platform The platform of the service.
+ * @param serviceId The id of the service.
+ * @param serverUuid The server uuid.
+ * @param enabled Whether or not data sending is enabled.
+ * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and
+ * appends all platform-specific data.
+ * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and
+ * appends all service-specific data.
+ * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be
+ * used to delegate the data collection to a another thread to prevent errors caused by
+ * concurrency. Can be {@code null}.
+ * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled.
+ * @param errorLogger A consumer that accepts log message and an error.
+ * @param infoLogger A consumer that accepts info log messages.
+ * @param logErrors Whether or not errors should be logged.
+ * @param logSentData Whether or not the sent data should be logged.
+ * @param logResponseStatusText Whether or not the response status text should be logged.
+ */
+ public MetricsBase(
+ final String platform,
+ final String serverUuid,
+ final int serviceId,
+ final boolean enabled,
+ final Consumer appendPlatformDataConsumer,
+ final Consumer appendServiceDataConsumer,
+ final Consumer submitTaskConsumer,
+ final Supplier checkServiceEnabledSupplier,
+ final BiConsumer errorLogger,
+ final Consumer infoLogger,
+ final boolean logErrors,
+ final boolean logSentData,
+ final boolean logResponseStatusText) {
+ this.platform = platform;
+ this.serverUuid = serverUuid;
+ this.serviceId = serviceId;
+ this.enabled = enabled;
+ this.appendPlatformDataConsumer = appendPlatformDataConsumer;
+ this.appendServiceDataConsumer = appendServiceDataConsumer;
+ this.submitTaskConsumer = submitTaskConsumer;
+ this.checkServiceEnabledSupplier = checkServiceEnabledSupplier;
+ this.errorLogger = errorLogger;
+ this.infoLogger = infoLogger;
+ this.logErrors = logErrors;
+ this.logSentData = logSentData;
+ this.logResponseStatusText = logResponseStatusText;
+ checkRelocation();
+ if (enabled) {
+ startSubmitting();
+ }
+ }
- // Should the sent data be logged?
- private static boolean logSentData;
+ public void addCustomChart(final CustomChart chart) {
+ this.customCharts.add(chart);
+ }
- // Should the response text be logged?
- private static boolean logResponseStatusText;
+ private void startSubmitting() {
+ final Runnable submitTask =
+ () -> {
+ if (!enabled || !checkServiceEnabledSupplier.get()) {
+ // Submitting data or service is disabled
+ scheduler.shutdown();
+ return;
+ }
+ if (submitTaskConsumer != null) {
+ submitTaskConsumer.accept(this::submitData);
+ } else {
+ this.submitData();
+ }
+ };
+ // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution
+ // of requests on the
+ // bStats backend. To circumvent this problem, we introduce some randomness into the initial
+ // and second delay.
+ // WARNING: You must not modify and part of this Metrics class, including the submit delay or
+ // frequency!
+ // WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it!
+ final long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3));
+ final long secondDelay = (long) (1000 * 60 * (Math.random() * 30));
+ scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS);
+ scheduler.scheduleAtFixedRate(
+ submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS);
+ }
- // The uuid of the server
- private static String serverUUID;
+ private void submitData() {
+ final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder();
+ appendPlatformDataConsumer.accept(baseJsonBuilder);
+ final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder();
+ appendServiceDataConsumer.accept(serviceJsonBuilder);
+ final JsonObjectBuilder.JsonObject[] chartData =
+ customCharts.stream()
+ .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors))
+ .filter(Objects::nonNull)
+ .toArray(JsonObjectBuilder.JsonObject[]::new);
+ serviceJsonBuilder.appendField("id", serviceId);
+ serviceJsonBuilder.appendField("customCharts", chartData);
+ baseJsonBuilder.appendField("service", serviceJsonBuilder.build());
+ baseJsonBuilder.appendField("serverUUID", serverUuid);
+ baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION);
+ final JsonObjectBuilder.JsonObject data = baseJsonBuilder.build();
+ scheduler.execute(
+ () -> {
+ try {
+ // Send the data
+ sendData(data);
+ } catch (final Exception e) {
+ // Something went wrong! :(
+ if (logErrors) {
+ errorLogger.accept("Could not submit bStats metrics data", e);
+ }
+ }
+ });
+ }
- // The plugin
- private final Plugin plugin;
+ private void sendData(final JsonObjectBuilder.JsonObject data) throws Exception {
+ if (logSentData) {
+ infoLogger.accept("Sent bStats metrics data: " + data.toString());
+ }
+ final String url = String.format(REPORT_URL, platform);
+ final HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
+ // Compress the data to save bandwidth
+ final byte[] compressedData = compress(data.toString());
+ connection.setRequestMethod("POST");
+ connection.addRequestProperty("Accept", "application/json");
+ connection.addRequestProperty("Connection", "close");
+ connection.addRequestProperty("Content-Encoding", "gzip");
+ connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length));
+ connection.setRequestProperty("Content-Type", "application/json");
+ connection.setRequestProperty("User-Agent", "Metrics-Service/1");
+ connection.setDoOutput(true);
+ try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
+ outputStream.write(compressedData);
+ }
+ final StringBuilder builder = new StringBuilder();
+ try (BufferedReader bufferedReader =
+ new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
+ String line;
+ while ((line = bufferedReader.readLine()) != null) {
+ builder.append(line);
+ }
+ }
+ if (logResponseStatusText) {
+ infoLogger.accept("Sent data to bStats and received response: " + builder);
+ }
+ }
- // The plugin id
- private final int pluginId;
+ /** Checks that the class was properly relocated. */
+ private void checkRelocation() {
+ // You can use the property to disable the check in your test environment
+ if (System.getProperty("bstats.relocatecheck") == null
+ || !System.getProperty("bstats.relocatecheck").equals("false")) {
+ // Maven's Relocate is clever and changes strings, too. So we have to use this little
+ // "trick" ... :D
+ final String defaultPackage =
+ new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'});
+ final String examplePackage =
+ new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'});
+ // We want to make sure no one just copy & pastes the example and uses the wrong package
+ // names
+ if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage)
+ || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) {
+ throw new IllegalStateException("bStats Metrics class has not been relocated correctly!");
+ }
+ }
+ }
- // A list with all custom charts
- private final List charts = new ArrayList<>();
+ /**
+ * Gzips the given string.
+ *
+ * @param str The string to gzip.
+ * @return The gzipped string.
+ */
+ private static byte[] compress(final String str) throws IOException {
+ if (str == null) {
+ return null;
+ }
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) {
+ gzip.write(str.getBytes(StandardCharsets.UTF_8));
+ }
+ return outputStream.toByteArray();
+ }
+ }
+
+ public static class AdvancedBarChart extends CustomChart {
+
+ private final Callable