From cb052e690cb37ea4913f1b39ab1cb21385dd1f41 Mon Sep 17 00:00:00 2001 From: Tyler Blair Date: Sat, 29 Jun 2013 10:47:05 -0300 Subject: Update Plugin Metrics to R7 --- .../com/earth2me/essentials/metrics/Metrics.java | 333 +++++++++++++++------ 1 file changed, 244 insertions(+), 89 deletions(-) diff --git a/Essentials/src/com/earth2me/essentials/metrics/Metrics.java b/Essentials/src/com/earth2me/essentials/metrics/Metrics.java index 77e68a9d6..4f0c588f0 100644 --- a/Essentials/src/com/earth2me/essentials/metrics/Metrics.java +++ b/Essentials/src/com/earth2me/essentials/metrics/Metrics.java @@ -23,6 +23,7 @@ package com.earth2me.essentials.metrics; * The views and conclusions contained in the software and documentation are those of the authors and contributors and * should not be interpreted as representing official policies, either expressed or implied, of anybody else. */ + import java.io.*; import java.net.Proxy; import java.net.URL; @@ -30,6 +31,7 @@ import java.net.URLConnection; import java.net.URLEncoder; import java.util.*; 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; @@ -37,71 +39,64 @@ import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.scheduler.BukkitTask; - -/** - *

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(Metrics.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://metrics.essentials3.net"; + private static final String BASE_URL = "http://report-metrics.essentials3.net"; + /** * 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 */ @@ -162,22 +157,18 @@ public class Metrics } /** - * Adds a custom data plotter to the default graph + * Add a Graph object to BukkitMetrics that represents data for the plugin that should be sent to the backend * - * @param plotter The plotter to use to plot custom data + * @param graph The name of the graph */ - public void addCustomData(final Plotter plotter) + public void addGraph(final Graph graph) { - if (plotter == null) + if (graph == null) { - throw new IllegalArgumentException("Plotter cannot be null"); + throw new IllegalArgumentException("Graph 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); + graphs.add(graph); } /** @@ -195,6 +186,12 @@ public class Metrics return; } + // Is metrics already running? + if (task != null) + { + return; + } + // Begin hitting the server with glorious data task = plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, new Runnable() { @@ -278,7 +275,7 @@ public class Metrics /** * Enables metrics for the server by setting "opt-out" to false in the config file and starting the metrics task. * - * @throws IOException + * @throws java.io.IOException */ public void enable() throws IOException { @@ -303,7 +300,7 @@ public class Metrics /** * Disables metrics for the server by setting "opt-out" to true in the config file and canceling the metrics task. * - * @throws IOException + * @throws java.io.IOException */ public void disable() throws IOException { @@ -360,14 +357,14 @@ public class Metrics // 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"); @@ -382,48 +379,69 @@ 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) + if (graphs.size() > 0) { - final Iterator iter = graphs.iterator(); - - while (iter.hasNext()) + synchronized (graphs) { - final Graph graph = iter.next(); + json.append(','); + json.append('"'); + json.append("graphs"); + json.append('"'); + json.append(':'); + json.append('{'); + + boolean firstGraph = true; - for (Plotter plotter : graph.getPlotters()) + final Iterator iter = graphs.iterator(); + + while (iter.hasNext()) { - // 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()); + Graph graph = iter.next(); + + StringBuilder graphJson = new StringBuilder(); + graphJson.append('{'); + + for (Plotter plotter : graph.getPlotters()) + { + appendJSONPair(graphJson, plotter.getColumnName(), Integer.toString(plotter.getValue())); + } - // The value to send, which for the foreseeable future is just the string - // value of plotter.getValue() - final String value = Integer.toString(plotter.getValue()); + graphJson.append('}'); - // Add it to the http post data :) - encodeDataPair(data, key, value); + 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; @@ -439,29 +457,55 @@ 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")) + if (response == null || response.startsWith("ERR") || response.startsWith("7")) { - throw new IOException(response); //Throw the exception + 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) { @@ -481,6 +525,43 @@ public class Metrics } } + /** + * 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 * @@ -500,21 +581,95 @@ 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 json + * @param key + * @param value + * @throws UnsupportedEncodingException + */ + private static void appendJSONPair(StringBuilder json, String key, String value) throws UnsupportedEncodingException + { + boolean isValueNumeric; + + try + { + 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 buffer the stringbuilder to append the data pair onto - * @param key the key value - * @param value the value + * @param text + * @return */ - private static void encodeDataPair(final StringBuilder buffer, final String key, final String value) throws UnsupportedEncodingException + private static String escapeJSON(String text) { - buffer.append('&').append(encode(key)).append('=').append(encode(value)); + 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 +678,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"); } @@ -582,7 +737,7 @@ public class Metrics /** * Gets an unmodifiable set of the plotter objects in the graph * - * @return an unmodifiable {@link Set} of the plotter objects + * @return an unmodifiable {@link java.util.Set} of the plotter objects */ public Set getPlotters() { @@ -608,7 +763,7 @@ public class Metrics } /** - * Called when the server owner decides to opt-out of Metrics while the server is running. + * Called when the server owner decides to opt-out of BukkitMetrics while the server is running. */ protected void onOptOut() { -- cgit v1.2.3