From 7a713e937972852166468df2679f73f600e3d5f2 Mon Sep 17 00:00:00 2001 From: Bastian Date: Tue, 26 Jan 2021 12:58:16 +0100 Subject: [PATCH] Update bStats and migrate to new config file This commit simplifies the Metrics class by using the new base module from bStats. It also migrates to the new bStats config file that will be used by plugins that integrate bStats. If a user disabled bStats in the old config file, this setting will be copied to the new config file. --- proxy/build.gradle | 4 + .../com/velocitypowered/proxy/Metrics.java | 693 +++--------------- .../velocitypowered/proxy/VelocityServer.java | 2 - .../proxy/config/VelocityConfiguration.java | 31 +- .../src/main/resources/default-velocity.toml | 13 - 5 files changed, 104 insertions(+), 639 deletions(-) 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.