3
0
Mirror von https://github.com/PaperMC/Velocity.git synchronisiert 2024-11-16 21:10:30 +01:00

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.
Dieser Commit ist enthalten in:
Bastian 2021-01-26 12:58:16 +01:00
Ursprung 3aee47166f
Commit 7a713e9379
5 geänderte Dateien mit 104 neuen und 639 gelöschten Zeilen

Datei anzeigen

@ -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 {

Datei anzeigen

@ -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.
* <p/>
* 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<CustomChart> 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<Response> 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<String> 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<String> 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<Map<String, Integer>> 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<Map<String, Integer>> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObject getChartData() throws Exception {
Map<String, Integer> 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<String, Integer> 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<Map<String, Map<String, Integer>>> 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<Map<String, Map<String, Integer>>> callable) {
super(chartId);
this.callable = callable;
}
@Override
public JsonObject getChartData() throws Exception {
Map<String, Map<String, Integer>> 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<String, Map<String, Integer>> entryValues : map.entrySet()) {
JsonObject value = new JsonObject();
boolean allSkipped = true;
for (Map.Entry<String, Integer> 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<Integer> 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<Integer> 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<Map<String, Integer>> 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<Map<String, Integer>> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObject getChartData() throws Exception {
Map<String, Integer> 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<String, Integer> 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<Map<String, Integer>> 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<Map<String, Integer>> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObject getChartData() throws Exception {
JsonObject data = new JsonObject();
JsonObject values = new JsonObject();
Map<String, Integer> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
for (Map.Entry<String, Integer> 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<Map<String, int[]>> 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<Map<String, int[]>> callable) {
super(chartId);
this.callable = callable;
}
@Override
protected JsonObject getChartData() throws Exception {
JsonObject values = new JsonObject();
Map<String, int[]> map = callable.call();
if (map == null || map.isEmpty()) {
// Null = skip the chart
return null;
}
boolean allSkipped = true;
for (Map.Entry<String, int[]> 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<String, Map<String, Integer>> map = new HashMap<>();
String javaVersion = System.getProperty("java.version");
Map<String, Integer> entry = new HashMap<>();
entry.put(javaVersion, 1);
metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> {
Map<String, Map<String, Integer>> map = new HashMap<>();
String javaVersion = System.getProperty("java.version");
Map<String, Integer> 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;
}));
}
}
}
}

Datei anzeigen

@ -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;

Datei anzeigen

@ -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.<String>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 {

Datei anzeigen

@ -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.