diff --git a/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java b/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java
new file mode 100644
index 000000000..b5de66eec
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java
@@ -0,0 +1,623 @@
+package com.velocitypowered.proxy;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.velocitypowered.proxy.config.VelocityConfiguration;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufOutputStream;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+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.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.GZIPOutputStream;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * 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
+ private 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 = LogManager.getLogger(Metrics.class);
+
+ // 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 charts = new ArrayList<>();
+
+ private final VelocityServer server;
+
+ /**
+ * Class constructor.
+ * @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 server The Velocity server instance.
+ */
+ private Metrics(String name, String serverUuid, boolean logFailedRequests,
+ VelocityServer server) {
+ this.name = name;
+ this.serverUuid = serverUuid;
+ Metrics.logFailedRequests = logFailedRequests;
+ this.server = server;
+
+ // 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() {
+ final Timer timer = new Timer(true);
+ timer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ submitData();
+ }
+ }, 1000, 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
+ 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
+ ByteBuf reqBody = createResponseBody(data);
+
+ server.getHttpClient().post(new URL(URL), reqBody, request -> {
+ request.headers().add(HttpHeaderNames.CONTENT_ENCODING, "gzip");
+ request.headers().add(HttpHeaderNames.ACCEPT, "application/json");
+ request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/json");
+ })
+ .whenCompleteAsync((resp, exc) -> {
+ if (logFailedRequests) {
+ if (exc != null) {
+ logger.error("Unable to send metrics to bStats", exc);
+ } else if (resp.getCode() != 429) {
+ logger.error("Got HTTP status code {} when sending metrics to bStats",
+ resp.getCode());
+ }
+ }
+ });
+ }
+
+ private static ByteBuf createResponseBody(JsonObject object) throws IOException {
+ ByteBuf buf = Unpooled.buffer();
+ try (Writer writer =
+ new BufferedWriter(
+ new OutputStreamWriter(
+ new GZIPOutputStream(new ByteBufOutputStream(buf)), StandardCharsets.UTF_8
+ )
+ )
+ ) {
+ VelocityServer.GSON.toJson(object, writer);
+ } catch (IOException e) {
+ buf.release();
+ throw e;
+ }
+ return buf;
+ }
+
+ /**
+ * 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) {
+ if (logFailedRequests) {
+ logger.warn("Failed to get data for custom chart with id {}", chartId, 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