From ce3069d490d4db99f179d1c32df3c4ab3b532296 Mon Sep 17 00:00:00 2001 From: Dan Mulloy Date: Thu, 9 Apr 2015 20:36:57 -0400 Subject: [PATCH] Update metrics to revision 7, don't include build number with metrics --- .../comphenix/protocol/metrics/Metrics.java | 328 +++++++++++------- 1 file changed, 212 insertions(+), 116 deletions(-) diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Metrics.java b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Metrics.java index d9f8930c..d029369e 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Metrics.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/metrics/Metrics.java @@ -1,24 +1,5 @@ /* - * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. - * Copyright (C) 2012 Kristian S. Stangeland - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 2 of - * the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with this program; - * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA - * 02111-1307 USA - */ - -package com.comphenix.protocol.metrics; - -/* - * Copyright 2011 Tyler Blair. All rights reserved. + * Copyright 2011-2013 Tyler Blair. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: @@ -44,12 +25,14 @@ package com.comphenix.protocol.metrics; * authors and contributors and should not be interpreted as representing official policies, * either expressed or implied, of anybody else. */ +package com.comphenix.protocol.metrics; import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; -import java.io.OutputStreamWriter; +import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.Proxy; import java.net.URL; @@ -62,84 +45,78 @@ import java.util.LinkedHashSet; import java.util.Set; import java.util.UUID; import java.util.logging.Level; +import java.util.zip.GZIPOutputStream; import org.bukkit.Bukkit; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.scheduler.BukkitTask; import com.comphenix.protocol.utility.Util; -import com.comphenix.protocol.utility.WrappedScheduler; -/** - *

The metrics class obtains data about a plugin and submits statistics about it to the metrics backend.

- * Public methods provided by this class:

- * - * Graph createGraph(String name);
- * void addCustomData(BukkitMetrics.Plotter plotter);
- * void start();
- *
- */ public class Metrics { /** * The current revision number */ - private final static int REVISION = 6; + private final static int REVISION = 7; + /** * The base url of the metrics domain */ - private static final String BASE_URL = "http://mcstats.org"; + private static final String BASE_URL = "http://report.mcstats.org"; + /** * The url used to report a server's status */ - private static final String REPORT_URL = "/report/%s"; - /** - * The separator to use for custom data. This MUST NOT change unless you are hosting your own version of metrics and - * want to change it. - */ - private static final String CUSTOM_DATA_SEPARATOR = "~~"; + private static final String REPORT_URL = "/plugin/%s"; + /** * Interval of time to ping (in minutes) */ - private static final int PING_INTERVAL = 10; + private static final int PING_INTERVAL = 15; + /** * The plugin this metrics submits for */ private final Plugin plugin; + /** * All of the custom graphs to submit to metrics */ private final Set graphs = Collections.synchronizedSet(new HashSet()); - /** - * The default graph, used for addCustomData when you don't want a specific graph - */ - private final Graph defaultGraph = new Graph("Default"); + /** * The plugin configuration file */ private final YamlConfiguration configuration; + /** * The plugin configuration file */ private final File configurationFile; + /** * Unique server id */ private final String guid; + /** * Debug mode */ private final boolean debug; + /** * Lock for synchronization */ private final Object optOutLock = new Object(); + /** * The scheduled task */ - private volatile WrappedScheduler.TaskWrapper task = null; + private volatile BukkitTask task = null; public Metrics(final Plugin plugin) throws IOException { if (plugin == null) { @@ -203,23 +180,6 @@ public class Metrics { graphs.add(graph); } - /** - * Adds a custom data plotter to the default graph - * - * @param plotter The plotter to use to plot custom data - */ - public void addCustomData(final Plotter plotter) { - if (plotter == null) { - throw new IllegalArgumentException("Plotter cannot be null"); - } - - // Add the plotter to the graph o/ - defaultGraph.addPlotter(plotter); - - // Ensure the default graph is included in the submitted graphs - graphs.add(defaultGraph); - } - /** * Start measuring statistics. This will immediately create an async repeating task as the plugin and send the * initial data to the metrics backend, and then after that it will post in increments of PING_INTERVAL * 1200 @@ -240,7 +200,7 @@ public class Metrics { } // Begin hitting the server with glorious data - task = WrappedScheduler.runAsynchronouslyRepeat(plugin, new Runnable() { + task = plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, new Runnable() { private boolean firstPost = true; @@ -373,21 +333,21 @@ public class Metrics { PluginDescriptionFile description = plugin.getDescription(); String pluginName = description.getName(); boolean onlineMode = Bukkit.getServer().getOnlineMode(); // TRUE if online mode is enabled - String pluginVersion = description.getVersion(); + String pluginVersion = trimVersion(description.getVersion()); // ProtocolLib String serverVersion = Bukkit.getVersion(); - int playersOnline = Util.getOnlinePlayers().size(); + int playersOnline = Util.getOnlinePlayers().size(); // ProtocolLib // END server software specific section -- all code below does not use any code outside of this class / Java // Construct the post data - final StringBuilder data = new StringBuilder(); + StringBuilder json = new StringBuilder(1024); + json.append('{'); // The plugin's description file containg all of the plugin data such as name, version, author, etc - data.append(encode("guid")).append('=').append(encode(guid)); - encodeDataPair(data, "version", pluginVersion); - encodeDataPair(data, "server", serverVersion); - encodeDataPair(data, "players", Integer.toString(playersOnline)); - encodeDataPair(data, "revision", String.valueOf(REVISION)); + appendJSONPair(json, "guid", guid); + appendJSONPair(json, "plugin_version", pluginVersion); + appendJSONPair(json, "server_version", serverVersion); + appendJSONPair(json, "players_online", Integer.toString(playersOnline)); // New data as of R6 String osname = System.getProperty("os.name"); @@ -401,44 +361,63 @@ public class Metrics { osarch = "x86_64"; } - encodeDataPair(data, "osname", osname); - encodeDataPair(data, "osarch", osarch); - encodeDataPair(data, "osversion", osversion); - encodeDataPair(data, "cores", Integer.toString(coreCount)); - encodeDataPair(data, "online-mode", Boolean.toString(onlineMode)); - encodeDataPair(data, "java_version", java_version); + appendJSONPair(json, "osname", osname); + appendJSONPair(json, "osarch", osarch); + appendJSONPair(json, "osversion", osversion); + appendJSONPair(json, "cores", Integer.toString(coreCount)); + appendJSONPair(json, "auth_mode", onlineMode ? "1" : "0"); + appendJSONPair(json, "java_version", java_version); // If we're pinging, append it if (isPing) { - encodeDataPair(data, "ping", "true"); + appendJSONPair(json, "ping", "1"); } - // Acquire a lock on the graphs, which lets us make the assumption we also lock everything - // inside of the graph (e.g plotters) - synchronized (graphs) { - final Iterator iter = graphs.iterator(); + if (graphs.size() > 0) { + synchronized (graphs) { + json.append(','); + json.append('"'); + json.append("graphs"); + json.append('"'); + json.append(':'); + json.append('{'); - while (iter.hasNext()) { - final Graph graph = iter.next(); + boolean firstGraph = true; - for (Plotter plotter : graph.getPlotters()) { - // The key name to send to the metrics server - // The format is C-GRAPHNAME-PLOTTERNAME where separator - is defined at the top - // Legacy (R4) submitters use the format Custom%s, or CustomPLOTTERNAME - final String key = String.format("C%s%s%s%s", CUSTOM_DATA_SEPARATOR, graph.getName(), CUSTOM_DATA_SEPARATOR, plotter.getColumnName()); + final Iterator iter = graphs.iterator(); - // The value to send, which for the foreseeable future is just the string - // value of plotter.getValue() - final String value = Integer.toString(plotter.getValue()); + while (iter.hasNext()) { + Graph graph = iter.next(); - // Add it to the http post data :) - encodeDataPair(data, key, value); + StringBuilder graphJson = new StringBuilder(); + graphJson.append('{'); + + for (Plotter plotter : graph.getPlotters()) { + appendJSONPair(graphJson, plotter.getColumnName(), Integer.toString(plotter.getValue())); + } + + graphJson.append('}'); + + if (!firstGraph) { + json.append(','); + } + + json.append(escapeJSON(graph.getName())); + json.append(':'); + json.append(graphJson); + + firstGraph = false; } + + json.append('}'); } } + // close json + json.append('}'); + // Create the url - URL url = new URL(BASE_URL + String.format(REPORT_URL, encode(pluginName))); + URL url = new URL(BASE_URL + String.format(REPORT_URL, urlEncode(pluginName))); // Connect to the website URLConnection connection; @@ -451,26 +430,48 @@ public class Metrics { connection = url.openConnection(); } + + byte[] uncompressed = json.toString().getBytes(); + byte[] compressed = gzip(json.toString()); + + // Headers + connection.addRequestProperty("User-Agent", "MCStats/" + REVISION); + connection.addRequestProperty("Content-Type", "application/json"); + connection.addRequestProperty("Content-Encoding", "gzip"); + connection.addRequestProperty("Content-Length", Integer.toString(compressed.length)); + connection.addRequestProperty("Accept", "application/json"); + connection.addRequestProperty("Connection", "close"); + connection.setDoOutput(true); + if (debug) { + System.out.println("[Metrics] Prepared request for " + pluginName + " uncompressed=" + uncompressed.length + " compressed=" + compressed.length); + } + // Write the data - final OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); - writer.write(data.toString()); - writer.flush(); + OutputStream os = connection.getOutputStream(); + os.write(compressed); + os.flush(); // Now read the response final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); - final String response = reader.readLine(); + String response = reader.readLine(); // close resources - writer.close(); + os.close(); reader.close(); - if (response == null || response.startsWith("ERR")) { - throw new IOException(response); //Throw the exception + if (response == null || response.startsWith("ERR") || response.startsWith("7")) { + if (response == null) { + response = "null"; + } else if (response.startsWith("7")) { + response = response.substring(response.startsWith("7,") ? 2 : 1); + } + + throw new IOException(response); } else { // Is this the first update this hour? - if (response.contains("OK This is your first update this hour")) { + if (response.equals("1") || response.contains("This is your first update this hour")) { synchronized (graphs) { final Iterator iter = graphs.iterator(); @@ -486,6 +487,37 @@ public class Metrics { } } + // Trim the version string to not include the build number + // Not doing this has an interesting effect on the version graph + private String trimVersion(String version) { + return version.contains("-b") ? version.substring(0, version.indexOf("-b")) : version; + } + + /** + * GZip compress a string of bytes + * + * @param input + * @return + */ + public static byte[] gzip(String input) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GZIPOutputStream gzos = null; + + try { + gzos = new GZIPOutputStream(baos); + gzos.write(input.getBytes("UTF-8")); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (gzos != null) try { + gzos.close(); + } catch (IOException ignore) { + } + } + + return baos.toByteArray(); + } + /** * Check if mineshafter is present. If it is, we need to bypass it to send POST requests * @@ -501,20 +533,83 @@ public class Metrics { } /** - *

Encode a key/value data pair to be used in a HTTP post request. This INCLUDES a & so the first key/value pair - * MUST be included manually, e.g:

- * - * StringBuffer data = new StringBuffer(); - * data.append(encode("guid")).append('=').append(encode(guid)); - * encodeDataPair(data, "version", description.getVersion()); - * + * Appends a json encoded key/value pair to the given string builder. * - * @param buffer the stringbuilder to append the data pair onto - * @param key the key value - * @param value the value + * @param json + * @param key + * @param value + * @throws UnsupportedEncodingException */ - private static void encodeDataPair(final StringBuilder buffer, final String key, final String value) throws UnsupportedEncodingException { - buffer.append('&').append(encode(key)).append('=').append(encode(value)); + private static void appendJSONPair(StringBuilder json, String key, String value) throws UnsupportedEncodingException { + boolean isValueNumeric = false; + + try { + if (value.equals("0") || !value.endsWith("0")) { + Double.parseDouble(value); + isValueNumeric = true; + } + } catch (NumberFormatException e) { + isValueNumeric = false; + } + + if (json.charAt(json.length() - 1) != '{') { + json.append(','); + } + + json.append(escapeJSON(key)); + json.append(':'); + + if (isValueNumeric) { + json.append(value); + } else { + json.append(escapeJSON(value)); + } + } + + /** + * Escape a string to create a valid JSON string + * + * @param text + * @return + */ + private static String escapeJSON(String text) { + StringBuilder builder = new StringBuilder(); + + builder.append('"'); + for (int index = 0; index < text.length(); index++) { + char chr = text.charAt(index); + + switch (chr) { + case '"': + case '\\': + builder.append('\\'); + builder.append(chr); + break; + case '\b': + builder.append("\\b"); + break; + case '\t': + builder.append("\\t"); + break; + case '\n': + builder.append("\\n"); + break; + case '\r': + builder.append("\\r"); + break; + default: + if (chr < ' ') { + String t = "000" + Integer.toHexString(chr); + builder.append("\\u" + t.substring(t.length() - 4)); + } else { + builder.append(chr); + } + break; + } + } + builder.append('"'); + + return builder.toString(); } /** @@ -523,7 +618,7 @@ public class Metrics { * @param text the text to encode * @return the encoded text, as UTF-8 */ - private static String encode(final String text) throws UnsupportedEncodingException { + private static String urlEncode(final String text) throws UnsupportedEncodingException { return URLEncoder.encode(text, "UTF-8"); } @@ -537,6 +632,7 @@ public class Metrics { * rejected */ private final String name; + /** * The set of plotters that are contained within this graph */