diff --git a/proxy/build.gradle b/proxy/build.gradle
index 25087f2e5..254d4bdea 100644
--- a/proxy/build.gradle
+++ b/proxy/build.gradle
@@ -76,6 +76,8 @@ dependencies {
implementation 'com.electronwill.night-config:toml:3.6.3'
+ implementation 'org.bstats:bstats-base:2.2.0'
+
compileOnly 'com.github.spotbugs:spotbugs-annotations:4.1.2'
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
@@ -126,6 +128,8 @@ shadowJar {
exclude 'it/unimi/dsi/fastutil/objects/*Reference*'
exclude 'it/unimi/dsi/fastutil/shorts/**'
exclude 'org/checkerframework/checker/**'
+
+ relocate 'org.bstats', 'com.velocitypowered.proxy.bstats'
}
artifacts {
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java b/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java
index d8d635afa..5728ca673 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java
@@ -1,85 +1,67 @@
package com.velocitypowered.proxy;
-import com.google.gson.JsonArray;
-import com.google.gson.JsonObject;
import com.velocitypowered.proxy.config.VelocityConfiguration;
-import io.netty.handler.codec.http.HttpHeaderNames;
-import java.io.BufferedWriter;
-import java.io.ByteArrayOutputStream;
+
+import java.io.File;
import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
+import java.nio.file.Paths;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import java.util.zip.GZIPOutputStream;
+
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.asynchttpclient.ListenableFuture;
-import org.asynchttpclient.Response;
+import org.bstats.MetricsBase;
+import org.bstats.charts.CustomChart;
+import org.bstats.charts.DrilldownPie;
+import org.bstats.charts.SimplePie;
+import org.bstats.charts.SingleLineChart;
+import org.bstats.config.MetricsConfig;
+import org.bstats.json.JsonObjectBuilder;
-/**
- * bStats collects some data for plugin authors.
- *
- * Check out https://bStats.org/ to learn more about bStats!
- */
public class Metrics {
- private static final int ONE_MINUTE_MS = 60_000;
+ private MetricsBase metricsBase;
- // The version of this bStats class
- private static final int B_STATS_METRICS_REVISION = 2;
+ private Metrics(Logger logger, int serviceId, boolean defaultEnabled) {
+ File configFile = Paths.get("plugins").resolve("bStats").resolve("config.txt").toFile();
+ MetricsConfig config;
+ try {
+ config = new MetricsConfig(configFile, defaultEnabled);
+ } catch (IOException e) {
+ logger.error("Failed to create bStats config", e);
+ return;
+ }
- // The url to which the data is sent
- private static final String URL = "https://bstats.org/submitData/server-implementation";
+ metricsBase = new MetricsBase(
+ "server-implementation",
+ config.getServerUUID(),
+ serviceId,
+ config.isEnabled(),
+ this::appendPlatformData,
+ jsonObjectBuilder -> { /* NOP */ },
+ null,
+ () -> true,
+ logger::warn,
+ logger::info,
+ config.isLogErrorsEnabled(),
+ config.isLogSentDataEnabled(),
+ config.isLogResponseStatusTextEnabled()
+ );
- // The logger for the failed requests
- private static final Logger logger = LogManager.getLogger(Metrics.class);
-
- // Should failed requests be logged?
- private boolean logFailedRequests = false;
-
- // The name of the server software
- private final String name;
-
- // The plugin ID for the server software as assigned by bStats.
- private final int pluginId;
-
- // The uuid of the server
- private final String serverUuid;
-
- // A list with all custom charts
- private final List charts = new ArrayList<>();
-
- private final VelocityServer server;
-
- /**
- * Class constructor.
- * @param name The name of the server software.
- * @param pluginId The plugin ID for the server software as assigned by bStats.
- * @param serverUuid The uuid of the server.
- * @param logFailedRequests Whether failed requests should be logged or not.
- * @param server The Velocity server instance.
- */
- private Metrics(String name, int pluginId, String serverUuid, boolean logFailedRequests,
- VelocityServer server) {
- this.name = name;
- this.pluginId = pluginId;
- this.serverUuid = serverUuid;
- this.logFailedRequests = logFailedRequests;
- this.server = server;
-
- // Start submitting the data
- startSubmitting();
+ if (!config.didExistBefore()) {
+ // Send an info message when the bStats config file gets created for the first time
+ logger.info("Velocity and some of its plugins collect metrics"
+ + " and send them to bStats (https://bStats.org).");
+ logger.info("bStats collects some basic information for plugin"
+ + " authors, like how many people use");
+ logger.info("their plugin and their total player count."
+ + " It's recommended to keep bStats enabled, but");
+ logger.info("if you're not comfortable with this, you can opt-out"
+ + " by editing the config.txt file in");
+ logger.info("the '/plugins/bStats/' folder and setting enabled to false.");
+ }
}
/**
@@ -88,546 +70,69 @@ public class Metrics {
* @param chart The chart to add.
*/
public void addCustomChart(CustomChart chart) {
- if (chart == null) {
- throw new IllegalArgumentException("Chart cannot be null!");
- }
- charts.add(chart);
+ metricsBase.addCustomChart(chart);
}
- /**
- * Starts the Scheduler which submits our data every 30 minutes.
- */
- private void startSubmitting() {
- final Timer timer = new Timer(true);
- long initialDelay = ONE_MINUTE_MS * 3 + (((long) (ThreadLocalRandom.current().nextDouble() * 2
- * ONE_MINUTE_MS)));
- timer.scheduleAtFixedRate(new TimerTask() {
- @Override
- public void run() {
- submitData();
- }
- }, initialDelay, 1000 * 60 * 30);
- // Submit the data every 30 minutes, first time after 5 minutes to give other plugins enough
- // time to start.
- //
- // WARNING: Changing the frequency has no effect but your plugin WILL be blocked/deleted!
- // WARNING: Just don't do it!
- }
-
- /**
- * Gets the plugin specific data.
- *
- * @return The plugin specific data.
- */
- private JsonObject getPluginData() {
- JsonObject data = new JsonObject();
-
- data.addProperty("pluginName", name); // Append the name of the server software
- data.addProperty("id", pluginId);
- data.addProperty("metricsRevision", B_STATS_METRICS_REVISION);
- JsonArray customCharts = new JsonArray();
- for (CustomChart customChart : charts) {
- // Add the data of the custom charts
- JsonObject chart = customChart.getRequestJsonObject();
- if (chart == null) { // If the chart is null, we skip it
- continue;
- }
- customCharts.add(chart);
- }
- data.add("customCharts", customCharts);
-
- return data;
- }
-
- /**
- * Gets the server specific data.
- *
- * @return The server specific data.
- */
- private JsonObject getServerData() {
- // OS specific data
- String osName = System.getProperty("os.name");
- String osArch = System.getProperty("os.arch");
- String osVersion = System.getProperty("os.version");
- int coreCount = Runtime.getRuntime().availableProcessors();
-
- JsonObject data = new JsonObject();
-
- data.addProperty("serverUUID", serverUuid);
-
- data.addProperty("osName", osName);
- data.addProperty("osArch", osArch);
- data.addProperty("osVersion", osVersion);
- data.addProperty("coreCount", coreCount);
-
- return data;
- }
-
- /**
- * Collects the data and sends it afterwards.
- */
- private void submitData() {
- final JsonObject data = getServerData();
-
- JsonArray pluginData = new JsonArray();
- pluginData.add(getPluginData());
- data.add("plugins", pluginData);
-
- try {
- // We are still in the Thread of the timer, so nothing get blocked :)
- sendData(data);
- } catch (Exception e) {
- // Something went wrong! :(
- if (logFailedRequests) {
- logger.warn("Could not submit stats of {}", name, e);
- }
- }
- }
-
- /**
- * Sends the data to the bStats server.
- *
- * @param data The data to send.
- * @throws Exception If the request failed.
- */
- private void sendData(JsonObject data) throws Exception {
- if (data == null) {
- throw new IllegalArgumentException("Data cannot be null!");
- }
-
- // Compress the data to save bandwidth
- ListenableFuture future = server.getAsyncHttpClient()
- .preparePost(URL)
- .addHeader(HttpHeaderNames.CONTENT_ENCODING, "gzip")
- .addHeader(HttpHeaderNames.ACCEPT, "application/json")
- .addHeader(HttpHeaderNames.CONTENT_TYPE, "application/json")
- .setBody(createResponseBody(data))
- .execute();
- future.addListener(() -> {
- if (logFailedRequests) {
- try {
- Response r = future.get();
- if (r.getStatusCode() != 429) {
- logger.error("Got HTTP status code {} when sending metrics to bStats",
- r.getStatusCode());
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- } catch (ExecutionException e) {
- logger.error("Unable to send metrics to bStats", e);
- }
- }
- }, null);
- }
-
- private static byte[] createResponseBody(JsonObject object) throws IOException {
- ByteArrayOutputStream os = new ByteArrayOutputStream();
- try (Writer writer =
- new BufferedWriter(
- new OutputStreamWriter(
- new GZIPOutputStream(os), StandardCharsets.UTF_8
- )
- )
- ) {
- VelocityServer.GENERAL_GSON.toJson(object, writer);
- }
- return os.toByteArray();
- }
-
- /**
- * Represents a custom chart.
- */
- public abstract static class CustomChart {
-
- // The id of the chart
- final String chartId;
-
- /**
- * Class constructor.
- *
- * @param chartId The id of the chart.
- */
- CustomChart(String chartId) {
- if (chartId == null || chartId.isEmpty()) {
- throw new IllegalArgumentException("ChartId cannot be null or empty!");
- }
- this.chartId = chartId;
- }
-
- private JsonObject getRequestJsonObject() {
- JsonObject chart = new JsonObject();
- chart.addProperty("chartId", chartId);
- try {
- JsonObject data = getChartData();
- if (data == null) {
- // If the data is null we don't send the chart.
- return null;
- }
- chart.add("data", data);
- } catch (Throwable t) {
- return null;
- }
- return chart;
- }
-
- protected abstract JsonObject getChartData() throws Exception;
-
- }
-
- /**
- * Represents a custom simple pie.
- */
- public static class SimplePie extends CustomChart {
-
- private final Callable callable;
-
- /**
- * Class constructor.
- *
- * @param chartId The id of the chart.
- * @param callable The callable which is used to request the chart data.
- */
- public SimplePie(String chartId, Callable callable) {
- super(chartId);
- this.callable = callable;
- }
-
- @Override
- protected JsonObject getChartData() throws Exception {
- JsonObject data = new JsonObject();
- String value = callable.call();
- if (value == null || value.isEmpty()) {
- // Null = skip the chart
- return null;
- }
- data.addProperty("value", value);
- return data;
- }
- }
-
- /**
- * Represents a custom advanced pie.
- */
- public static class AdvancedPie extends CustomChart {
-
- private final Callable