mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-08-14 23:57:35 +00:00
579 lines
No EOL
19 KiB
Java
579 lines
No EOL
19 KiB
Java
/*
|
|
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*
|
|
* @author GeyserMC
|
|
* @link https://github.com/GeyserMC/Geyser
|
|
*/
|
|
|
|
package org.geysermc.geyser.util;
|
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
import org.geysermc.geyser.GeyserImpl;
|
|
|
|
import javax.net.ssl.HttpsURLConnection;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.DataOutputStream;
|
|
import java.io.IOException;
|
|
import java.net.URL;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.concurrent.Callable;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
import java.util.zip.GZIPOutputStream;
|
|
|
|
/**
|
|
* bStats collects some data for plugin authors.
|
|
*
|
|
* Check out https://bStats.org/ to learn more about bStats!
|
|
*/
|
|
public class Metrics {
|
|
|
|
// The version of this bStats class
|
|
public static final int B_STATS_VERSION = 1;
|
|
|
|
// The url to which the data is sent
|
|
private static final String URL = "https://bStats.org/submitData/server-implementation";
|
|
|
|
// Should failed requests be logged?
|
|
private static boolean logFailedRequests = false;
|
|
|
|
// The logger for the failed requests
|
|
private static Logger logger = Logger.getLogger("bStats");
|
|
|
|
// The name of the server software
|
|
private final String name;
|
|
|
|
// The uuid of the server
|
|
private final String serverUUID;
|
|
|
|
// A list with all custom charts
|
|
private final List<CustomChart> charts = new ArrayList<>();
|
|
|
|
private final static ObjectMapper mapper = new ObjectMapper();
|
|
|
|
private final GeyserImpl geyser;
|
|
|
|
/**
|
|
* Class constructor.
|
|
*
|
|
* @param geyser The Geyser instance
|
|
* @param name The name of the server software.
|
|
* @param serverUUID The uuid of the server.
|
|
* @param logFailedRequests Whether failed requests should be logged or not.
|
|
* @param logger The logger for the failed requests.
|
|
*/
|
|
public Metrics(GeyserImpl geyser, String name, String serverUUID, boolean logFailedRequests, Logger logger) {
|
|
this.geyser = geyser;
|
|
this.name = name;
|
|
this.serverUUID = serverUUID;
|
|
Metrics.logFailedRequests = logFailedRequests;
|
|
Metrics.logger = logger;
|
|
|
|
// Start submitting the data
|
|
startSubmitting();
|
|
}
|
|
|
|
/**
|
|
* Adds a custom chart.
|
|
*
|
|
* @param chart The chart to add.
|
|
*/
|
|
public void addCustomChart(CustomChart chart) {
|
|
if (chart == null) {
|
|
throw new IllegalArgumentException("Chart cannot be null!");
|
|
}
|
|
charts.add(chart);
|
|
}
|
|
|
|
/**
|
|
* Starts the Scheduler which submits our data every 30 minutes.
|
|
*/
|
|
private void startSubmitting() {
|
|
geyser.getScheduledThread().scheduleAtFixedRate(this::submitData, 1, 30, TimeUnit.MINUTES);
|
|
// Submit the data every 30 minutes, first time after 1 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 ObjectNode getPluginData() {
|
|
ObjectNode data = mapper.createObjectNode();
|
|
|
|
data.put("pluginName", name); // Append the name of the server software
|
|
data.put("pluginVersion", GeyserImpl.VERSION); // Append the name of the server software
|
|
|
|
ArrayNode customCharts = mapper.createArrayNode();
|
|
for (CustomChart customChart : charts) {
|
|
// Add the data of the custom charts
|
|
JsonNode chart = customChart.getRequestJsonNode();
|
|
if (chart == null) { // If the chart is null, we skip it
|
|
continue;
|
|
}
|
|
customCharts.add(chart);
|
|
}
|
|
data.put("customCharts", customCharts);
|
|
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Gets the server specific data.
|
|
*
|
|
* @return The server specific data.
|
|
*/
|
|
private ObjectNode 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();
|
|
|
|
ObjectNode data = mapper.createObjectNode();
|
|
|
|
data.put("serverUUID", serverUUID);
|
|
|
|
data.put("osName", osName);
|
|
data.put("osArch", osArch);
|
|
data.put("osVersion", osVersion);
|
|
data.put("coreCount", coreCount);
|
|
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Collects the data and sends it afterwards.
|
|
*/
|
|
private void submitData() {
|
|
final ObjectNode data = getServerData();
|
|
|
|
ArrayNode pluginData = mapper.createArrayNode();
|
|
pluginData.add(getPluginData());
|
|
data.putPOJO("plugins", pluginData);
|
|
|
|
new Thread(() -> {
|
|
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.log(Level.WARNING, "Could not submit stats of " + name, e);
|
|
}
|
|
}
|
|
}).start();
|
|
}
|
|
|
|
/**
|
|
* Sends the data to the bStats server.
|
|
*
|
|
* @param data The data to send.
|
|
* @throws Exception If the request failed.
|
|
*/
|
|
private static void sendData(ObjectNode data) throws Exception {
|
|
if (data == null) {
|
|
throw new IllegalArgumentException("Data cannot be null!");
|
|
}
|
|
HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection();
|
|
|
|
// Compress the data to save bandwidth
|
|
byte[] compressedData = compress(data.toString());
|
|
|
|
// Add headers
|
|
connection.setRequestMethod("POST");
|
|
connection.addRequestProperty("Accept", "application/json");
|
|
connection.addRequestProperty("Connection", "close");
|
|
connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request
|
|
connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length));
|
|
connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format
|
|
connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION);
|
|
|
|
// Send data
|
|
connection.setDoOutput(true);
|
|
DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());
|
|
outputStream.write(compressedData);
|
|
outputStream.flush();
|
|
outputStream.close();
|
|
|
|
connection.getInputStream().close(); // We don't care about the response - Just send our data :)
|
|
}
|
|
|
|
/**
|
|
* Gzips the given String.
|
|
*
|
|
* @param str The string to gzip.
|
|
* @return The gzipped String.
|
|
* @throws IOException If the compression failed.
|
|
*/
|
|
private static byte[] compress(final String str) throws IOException {
|
|
if (str == null) {
|
|
return null;
|
|
}
|
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
GZIPOutputStream gzip = new GZIPOutputStream(outputStream);
|
|
gzip.write(str.getBytes(StandardCharsets.UTF_8));
|
|
gzip.close();
|
|
return outputStream.toByteArray();
|
|
}
|
|
|
|
/**
|
|
* Represents a custom chart.
|
|
*/
|
|
public static abstract 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 ObjectNode getRequestJsonNode() {
|
|
ObjectNode chart = new ObjectMapper().createObjectNode();
|
|
chart.put("chartId", chartId);
|
|
try {
|
|
ObjectNode data = getChartData();
|
|
if (data == null) {
|
|
// If the data is null we don't send the chart.
|
|
return null;
|
|
}
|
|
chart.putPOJO("data", data);
|
|
} catch (Throwable t) {
|
|
if (logFailedRequests) {
|
|
logger.log(Level.WARNING, "Failed to get data for custom chart with id " + chartId, t);
|
|
}
|
|
return null;
|
|
}
|
|
return chart;
|
|
}
|
|
|
|
|
|
|
|
protected abstract ObjectNode 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 ObjectNode getChartData() throws Exception {
|
|
ObjectNode data = mapper.createObjectNode();
|
|
String value = callable.call();
|
|
if (value == null || value.isEmpty()) {
|
|
// Null = skip the chart
|
|
return null;
|
|
}
|
|
data.put("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 ObjectNode getChartData() throws Exception {
|
|
ObjectNode data = mapper.createObjectNode();
|
|
ObjectNode values = mapper.createObjectNode();
|
|
Map<String, Integer> map = callable.call();
|
|
if (map == null || map.isEmpty()) {
|
|
// Null = skip the chart
|
|
return null;
|
|
}
|
|
boolean allSkipped = true;
|
|
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
|
if (entry.getValue() == 0) {
|
|
continue; // Skip this invalid
|
|
}
|
|
allSkipped = false;
|
|
values.put(entry.getKey(), entry.getValue());
|
|
}
|
|
if (allSkipped) {
|
|
// Null = skip the chart
|
|
return null;
|
|
}
|
|
data.putPOJO("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 ObjectNode getChartData() throws Exception {
|
|
ObjectNode data = mapper.createObjectNode();
|
|
ObjectNode values = mapper.createObjectNode();
|
|
Map<String, Map<String, Integer>> map = callable.call();
|
|
if (map == null || map.isEmpty()) {
|
|
// Null = skip the chart
|
|
return null;
|
|
}
|
|
boolean reallyAllSkipped = true;
|
|
for (Map.Entry<String, Map<String, Integer>> entryValues : map.entrySet()) {
|
|
ObjectNode value = mapper.createObjectNode();
|
|
boolean allSkipped = true;
|
|
for (Map.Entry<String, Integer> valueEntry : map.get(entryValues.getKey()).entrySet()) {
|
|
value.put(valueEntry.getKey(), valueEntry.getValue());
|
|
allSkipped = false;
|
|
}
|
|
if (!allSkipped) {
|
|
reallyAllSkipped = false;
|
|
values.putPOJO(entryValues.getKey(), value);
|
|
}
|
|
}
|
|
if (reallyAllSkipped) {
|
|
// Null = skip the chart
|
|
return null;
|
|
}
|
|
data.putPOJO("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 ObjectNode getChartData() throws Exception {
|
|
ObjectNode data = mapper.createObjectNode();
|
|
int value = callable.call();
|
|
if (value == 0) {
|
|
// Null = skip the chart
|
|
return null;
|
|
}
|
|
data.put("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 ObjectNode getChartData() throws Exception {
|
|
ObjectNode data = mapper.createObjectNode();
|
|
ObjectNode values = mapper.createObjectNode();
|
|
Map<String, Integer> map = callable.call();
|
|
if (map == null || map.isEmpty()) {
|
|
// Null = skip the chart
|
|
return null;
|
|
}
|
|
boolean allSkipped = true;
|
|
for (Map.Entry<String, Integer> entry : map.entrySet()) {
|
|
if (entry.getValue() == 0) {
|
|
continue; // Skip this invalid
|
|
}
|
|
allSkipped = false;
|
|
values.put(entry.getKey(), entry.getValue());
|
|
}
|
|
if (allSkipped) {
|
|
// Null = skip the chart
|
|
return null;
|
|
}
|
|
data.putPOJO("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 ObjectNode getChartData() throws Exception {
|
|
ObjectNode data = mapper.createObjectNode();
|
|
ObjectNode values = mapper.createObjectNode();
|
|
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()) {
|
|
ArrayNode categoryValues = mapper.createArrayNode();
|
|
categoryValues.add(entry.getValue());
|
|
values.putPOJO(entry.getKey(), categoryValues);
|
|
}
|
|
data.putPOJO("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 ObjectNode getChartData() throws Exception {
|
|
ObjectNode data = mapper.createObjectNode();
|
|
ObjectNode values = mapper.createObjectNode();
|
|
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;
|
|
ArrayNode categoryValues = mapper.createArrayNode();
|
|
for (int categoryValue : entry.getValue()) {
|
|
categoryValues.add(categoryValue);
|
|
}
|
|
values.putPOJO(entry.getKey(), categoryValues);
|
|
}
|
|
if (allSkipped) {
|
|
// Null = skip the chart
|
|
return null;
|
|
}
|
|
data.putPOJO("values", values);
|
|
return data;
|
|
}
|
|
|
|
}
|
|
} |