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> callable; - - /** - * Class constructor. - * - * @param chartId The id of the chart. - * @param callable The callable which is used to request the chart data. - */ - public AdvancedPie(String chartId, Callable> callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObject getChartData() throws Exception { - Map map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - - JsonObject data = new JsonObject(); - JsonObject values = new JsonObject(); - boolean allSkipped = true; - for (Map.Entry entry : map.entrySet()) { - if (entry.getValue() == 0) { - continue; // Skip this invalid - } - allSkipped = false; - values.addProperty(entry.getKey(), entry.getValue()); - } - if (allSkipped) { - // Null = skip the chart - return null; - } - data.add("values", values); - return data; - } - } - - /** - * Represents a custom drilldown pie. - */ - public static class DrilldownPie 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 DrilldownPie(String chartId, Callable>> callable) { - super(chartId); - this.callable = callable; - } - - @Override - public JsonObject getChartData() throws Exception { - Map> map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - boolean reallyAllSkipped = true; - JsonObject data = new JsonObject(); - JsonObject values = new JsonObject(); - for (Map.Entry> entryValues : map.entrySet()) { - JsonObject value = new JsonObject(); - boolean allSkipped = true; - for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { - value.addProperty(valueEntry.getKey(), valueEntry.getValue()); - allSkipped = false; - } - if (!allSkipped) { - reallyAllSkipped = false; - values.add(entryValues.getKey(), value); - } - } - if (reallyAllSkipped) { - // Null = skip the chart - return null; - } - data.add("values", values); - return data; - } - } - - /** - * Represents a custom single line chart. - */ - public static class SingleLineChart 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 SingleLineChart(String chartId, Callable callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObject getChartData() throws Exception { - JsonObject data = new JsonObject(); - int value = callable.call(); - if (value == 0) { - // Null = skip the chart - return null; - } - data.addProperty("value", value); - return data; - } - - } - - /** - * Represents a custom multi line chart. - */ - public static class MultiLineChart 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 MultiLineChart(String chartId, Callable> callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObject getChartData() throws Exception { - Map map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - JsonObject data = new JsonObject(); - JsonObject values = new JsonObject(); - boolean allSkipped = true; - for (Map.Entry entry : map.entrySet()) { - if (entry.getValue() == 0) { - continue; // Skip this invalid - } - allSkipped = false; - values.addProperty(entry.getKey(), entry.getValue()); - } - if (allSkipped) { - // Null = skip the chart - return null; - } - data.add("values", values); - return data; - } - - } - - /** - * Represents a custom simple bar chart. - */ - public static class SimpleBarChart 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 SimpleBarChart(String chartId, Callable> callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObject getChartData() throws Exception { - JsonObject data = new JsonObject(); - JsonObject values = new JsonObject(); - Map map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - for (Map.Entry entry : map.entrySet()) { - JsonArray categoryValues = new JsonArray(); - categoryValues.add(entry.getValue()); - values.add(entry.getKey(), categoryValues); - } - data.add("values", values); - return data; - } - - } - - /** - * Represents a custom advanced bar chart. - */ - public static class AdvancedBarChart 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 AdvancedBarChart(String chartId, Callable> callable) { - super(chartId); - this.callable = callable; - } - - @Override - protected JsonObject getChartData() throws Exception { - JsonObject values = new JsonObject(); - Map map = callable.call(); - if (map == null || map.isEmpty()) { - // Null = skip the chart - return null; - } - boolean allSkipped = true; - for (Map.Entry entry : map.entrySet()) { - if (entry.getValue().length == 0) { - continue; // Skip this invalid - } - allSkipped = false; - JsonArray categoryValues = new JsonArray(); - for (int categoryValue : entry.getValue()) { - categoryValues.add(categoryValue); - } - values.add(entry.getKey(), categoryValues); - } - if (allSkipped) { - // Null = skip the chart - return null; - } - JsonObject data = new JsonObject(); - data.add("values", values); - return data; - } - + private void appendPlatformData(JsonObjectBuilder builder) { + 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()); } static class VelocityMetrics { + + private static final Logger logger = LogManager.getLogger(Metrics.class); + static void startMetrics(VelocityServer server, VelocityConfiguration.Metrics metricsConfig) { - if (!metricsConfig.isFromConfig()) { - // Log an informational message. - logger.info("Velocity collects metrics and sends them to bStats (https://bstats.org)."); - logger.info("bStats collects some basic information like how many people use Velocity and"); - logger.info("their player count. This has no impact on performance and this data does not"); - logger.info("identify your server in any way. However, you may opt-out by editing your"); - logger.info("velocity.toml and setting enabled = false in the [metrics] section."); - } + Metrics metrics = new Metrics(logger, 4752, metricsConfig.isEnabled()); - // Load the data - String serverUuid = metricsConfig.getId(); - boolean logFailedRequests = metricsConfig.isLogFailure(); - // Only start Metrics, if it's enabled in the config - if (metricsConfig.isEnabled()) { - Metrics metrics = new Metrics("Velocity", 4752, serverUuid, logFailedRequests, server); + metrics.addCustomChart( + new SingleLineChart("players", server::getPlayerCount) + ); + metrics.addCustomChart( + new SingleLineChart("managed_servers", () -> server.getAllServers().size()) + ); + metrics.addCustomChart( + new SimplePie("online_mode", + () -> server.getConfiguration().isOnlineMode() ? "online" : "offline") + ); + metrics.addCustomChart(new SimplePie("velocity_version", + () -> server.getVersion().getVersion())); - metrics.addCustomChart( - new Metrics.SingleLineChart("players", server::getPlayerCount) - ); - metrics.addCustomChart( - new Metrics.SingleLineChart("managed_servers", () -> server.getAllServers().size()) - ); - metrics.addCustomChart( - new Metrics.SimplePie("online_mode", - () -> server.getConfiguration().isOnlineMode() ? "online" : "offline") - ); - metrics.addCustomChart(new Metrics.SimplePie("velocity_version", - () -> server.getVersion().getVersion())); + metrics.addCustomChart(new DrilldownPie("java_version", () -> { + Map> map = new HashMap<>(); + String javaVersion = System.getProperty("java.version"); + Map entry = new HashMap<>(); + entry.put(javaVersion, 1); - metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { - Map> map = new HashMap<>(); - String javaVersion = System.getProperty("java.version"); - Map entry = new HashMap<>(); - entry.put(javaVersion, 1); + // http://openjdk.java.net/jeps/223 + // Java decided to change their versioning scheme and in doing so modified the + // java.version system property to return $major[.$minor][.$security][-ea], as opposed to + // 1.$major.0_$identifier we can handle pre-9 by checking if the "major" is equal to "1", + // otherwise, 9+ + String majorVersion = javaVersion.split("\\.")[0]; + String release; - // http://openjdk.java.net/jeps/223 - // Java decided to change their versioning scheme and in doing so modified the - // java.version system property to return $major[.$minor][.$security][-ea], as opposed to - // 1.$major.0_$identifier we can handle pre-9 by checking if the "major" is equal to "1", - // otherwise, 9+ - String majorVersion = javaVersion.split("\\.")[0]; - String release; + int indexOf = javaVersion.lastIndexOf('.'); - int indexOf = javaVersion.lastIndexOf('.'); - - if (majorVersion.equals("1")) { - release = "Java " + javaVersion.substring(0, indexOf); - } else { - // of course, it really wouldn't be all that simple if they didn't add a quirk, now - // would it valid strings for the major may potentially include values such as -ea to - // denote a pre release - Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion); - if (versionMatcher.find()) { - majorVersion = versionMatcher.group(0); - } - release = "Java " + majorVersion; + if (majorVersion.equals("1")) { + release = "Java " + javaVersion.substring(0, indexOf); + } else { + // of course, it really wouldn't be all that simple if they didn't add a quirk, now + // would it valid strings for the major may potentially include values such as -ea to + // denote a pre release + Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion); + if (versionMatcher.find()) { + majorVersion = versionMatcher.group(0); } - map.put(release, entry); - - return map; - })); - } + release = "Java " + majorVersion; + } + map.put(release, entry); + return map; + })); } } -} + +} \ No newline at end of file diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 863a481c7..12564cd9f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -77,8 +77,6 @@ import java.util.stream.Collectors; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.ForwardingAudience; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.TextComponent; -import net.kyori.adventure.text.TranslatableComponent; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.asynchttpclient.AsyncHttpClient; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java index 803312423..fa455e886 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -29,7 +29,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Random; -import java.util.UUID; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; @@ -436,11 +435,6 @@ public class VelocityConfiguration implements ProxyConfig { } forwardingSecret = forwardingSecretString.getBytes(StandardCharsets.UTF_8); - if (!config.contains("metrics.id") || config.get("metrics.id").isEmpty()) { - config.set("metrics.id", UUID.randomUUID().toString()); - mustResave = true; - } - if (mustResave) { config.save(); } @@ -783,39 +777,16 @@ public class VelocityConfiguration implements ProxyConfig { public static class Metrics { private boolean enabled = true; - private String id = UUID.randomUUID().toString(); - private boolean logFailure = false; - - private boolean fromConfig; - - private Metrics() { - this.fromConfig = false; - } private Metrics(CommentedConfig toml) { if (toml != null) { - this.enabled = toml.getOrElse("enabled", false); - this.id = toml.getOrElse("id", UUID.randomUUID().toString()); - this.logFailure = toml.getOrElse("log-failure", false); - this.fromConfig = true; + this.enabled = toml.getOrElse("enabled", true); } } public boolean isEnabled() { return enabled; } - - public String getId() { - return id; - } - - public boolean isLogFailure() { - return logFailure; - } - - public boolean isFromConfig() { - return fromConfig; - } } public static class Messages { diff --git a/proxy/src/main/resources/default-velocity.toml b/proxy/src/main/resources/default-velocity.toml index f3b890926..fbad6626c 100644 --- a/proxy/src/main/resources/default-velocity.toml +++ b/proxy/src/main/resources/default-velocity.toml @@ -143,19 +143,6 @@ map = "Velocity" # Whether plugins should be shown in query response by default or not show-plugins = false -[metrics] -# Whether metrics will be reported to bStats (https://bstats.org). -# bStats collects some basic information, like how many people use Velocity and their -# player count. We recommend keeping bStats enabled, but if you're not comfortable with -# this, you can turn this setting off. There is no performance penalty associated with -# having metrics enabled, and data sent to bStats can't identify your server. -enabled = true - -# A unique, anonymous ID to identify this proxy with. -id = "" - -log-failure = false - # Legacy color codes and JSON are accepted in all messages. [messages] # Prefix when the player gets kicked from a server.