SLING-5693 - Add support for exporting MetricRegistry data as JSON

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1740952 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index c76858f..7b9fef1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -113,6 +113,12 @@
       <version>2.2</version>
       <optional>true</optional>
     </dependency>
+    <dependency>
+      <groupId>org.json</groupId>
+      <artifactId>json</artifactId>
+      <version>20090211</version>
+      <optional>true</optional>
+    </dependency>
 
     <dependency>
       <groupId>org.hamcrest</groupId>
diff --git a/src/main/java/org/apache/sling/commons/metrics/internal/JSONReporter.java b/src/main/java/org/apache/sling/commons/metrics/internal/JSONReporter.java
new file mode 100644
index 0000000..9c631f7
--- /dev/null
+++ b/src/main/java/org/apache/sling/commons/metrics/internal/JSONReporter.java
@@ -0,0 +1,298 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sling.commons.metrics.internal;
+
+import java.io.Closeable;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.util.Locale;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.concurrent.TimeUnit;
+
+import com.codahale.metrics.ConsoleReporter;
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricFilter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Reporter;
+import com.codahale.metrics.Snapshot;
+import com.codahale.metrics.Timer;
+import org.json.JSONException;
+import org.json.JSONWriter;
+
+class JSONReporter implements Reporter, Closeable {
+
+    public static JSONReporter.Builder forRegistry(MetricRegistry registry) {
+        return new JSONReporter.Builder(registry);
+    }
+
+    public static class Builder {
+        private final MetricRegistry registry;
+        private PrintStream output;
+        private MetricFilter filter;
+        private TimeUnit rateUnit;
+        private TimeUnit durationUnit;
+
+        private Builder(MetricRegistry registry) {
+            this.registry = registry;
+            this.output = System.out;
+            this.filter = MetricFilter.ALL;
+            this.rateUnit = TimeUnit.SECONDS;
+            this.durationUnit = TimeUnit.MILLISECONDS;
+        }
+
+        /**
+         * Write to the given {@link PrintStream}.
+         *
+         * @param output a {@link PrintStream} instance.
+         * @return {@code this}
+         */
+        public Builder outputTo(PrintStream output) {
+            this.output = output;
+            return this;
+        }
+
+        /**
+         * Convert rates to the given time unit.
+         *
+         * @param rateUnit a unit of time
+         * @return {@code this}
+         */
+        public Builder convertRatesTo(TimeUnit rateUnit) {
+            this.rateUnit = rateUnit;
+            return this;
+        }
+
+        /**
+         * Convert durations to the given time unit.
+         *
+         * @param durationUnit a unit of time
+         * @return {@code this}
+         */
+        public Builder convertDurationsTo(TimeUnit durationUnit) {
+            this.durationUnit = durationUnit;
+            return this;
+        }
+
+        /**
+         * Only report metrics which match the given filter.
+         *
+         * @param filter a {@link MetricFilter}
+         * @return {@code this}
+         */
+        public Builder filter(MetricFilter filter) {
+            this.filter = filter;
+            return this;
+        }
+
+        /**
+         * Builds a {@link ConsoleReporter} with the given properties.
+         *
+         * @return a {@link ConsoleReporter}
+         */
+        public JSONReporter build() {
+            return new JSONReporter(registry,
+                    output,
+                    rateUnit,
+                    durationUnit,
+                    filter);
+        }
+    }
+
+    private final MetricRegistry registry;
+    private final MetricFilter filter;
+    private final double durationFactor;
+    private final String durationUnit;
+    private final double rateFactor;
+    private final String rateUnit;
+    private final JSONWriter json;
+    private final PrintWriter pw;
+
+    private JSONReporter(MetricRegistry registry,
+                         PrintStream output, TimeUnit rateUnit, TimeUnit durationUnit, MetricFilter filter){
+        this.registry = registry;
+        this.filter = filter;
+        this.pw = new PrintWriter(output);
+        this.json = new JSONWriter(pw);
+        this.rateFactor = rateUnit.toSeconds(1);
+        this.rateUnit = calculateRateUnit(rateUnit);
+        this.durationFactor = 1.0 / durationUnit.toNanos(1);
+        this.durationUnit = durationUnit.toString().toLowerCase(Locale.US);
+    }
+
+    public void report() {
+        try {
+            report(registry.getGauges(filter),
+                    registry.getCounters(filter),
+                    registry.getHistograms(filter),
+                    registry.getMeters(filter),
+                    registry.getTimers(filter));
+        } catch (JSONException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void close(){
+        pw.close();
+    }
+
+    private void report(SortedMap<String, Gauge> gauges, SortedMap<String, Counter> counters,
+                        SortedMap<String, Histogram> histograms, SortedMap<String, Meter> meters,
+                        SortedMap<String, Timer> timers) throws JSONException {
+        json.object();
+        if (!gauges.isEmpty()) {
+            json.key("guages").object();
+            for (Map.Entry<String, Gauge> entry : gauges.entrySet()) {
+                printGauge(entry);
+            }
+            json.endObject();
+        }
+
+        if (!counters.isEmpty()) {
+            json.key("counters").object();
+            for (Map.Entry<String, Counter> entry : counters.entrySet()) {
+                printCounter(entry);
+            }
+            json.endObject();
+        }
+
+        if (!histograms.isEmpty()) {
+            json.key("histograms").object();
+            for (Map.Entry<String, Histogram> entry : histograms.entrySet()) {
+                printHistogram(entry);
+            }
+            json.endObject();
+        }
+
+        if (!meters.isEmpty()) {
+            json.key("meters").object();
+            for (Map.Entry<String, Meter> entry : meters.entrySet()) {
+                printMeter(entry);
+            }
+            json.endObject();
+        }
+
+        if (!timers.isEmpty()) {
+            json.key("timers").object();
+            for (Map.Entry<String, Timer> entry : timers.entrySet()) {
+                printTimer(entry);
+            }
+            json.endObject();
+        }
+
+        json.endObject();
+
+    }
+
+    private void printTimer(Map.Entry<String, Timer> e) throws JSONException {
+        json.key(e.getKey()).object();
+        Timer timer = e.getValue();
+        Snapshot snapshot = timer.getSnapshot();
+
+        json.key("count").value(timer.getCount());
+        json.key("max").value(snapshot.getMax() * durationFactor);
+        json.key("mean").value(snapshot.getMean() * durationFactor);
+        json.key("min").value(snapshot.getMin() * durationFactor);
+
+        json.key("p50").value(snapshot.getMedian() * durationFactor);
+        json.key("p75").value(snapshot.get75thPercentile() * durationFactor);
+        json.key("p95").value(snapshot.get95thPercentile() * durationFactor);
+        json.key("p98").value(snapshot.get98thPercentile() * durationFactor);
+        json.key("p99").value(snapshot.get99thPercentile() * durationFactor);
+        json.key("p999").value(snapshot.get999thPercentile() * durationFactor);
+
+        json.key("stddev").value(snapshot.getStdDev() * durationFactor);
+        json.key("m1_rate").value(timer.getOneMinuteRate() * rateFactor);
+        json.key("m5_rate").value(timer.getFiveMinuteRate() * rateFactor);
+        json.key("m15_rate").value(timer.getFifteenMinuteRate() * rateFactor);
+        json.key("mean_rate").value(timer.getMeanRate() * rateFactor);
+        json.key("duration_units").value(durationUnit);
+        json.key("rate_units").value(rateUnit);
+
+        json.endObject();
+    }
+
+    private void printMeter(Map.Entry<String, Meter> e) throws JSONException {
+        json.key(e.getKey()).object();
+        Meter meter = e.getValue();
+        json.key("count").value(e.getValue().getCount());
+        json.key("m1_rate").value(meter.getOneMinuteRate() * rateFactor);
+        json.key("m5_rate").value(meter.getFiveMinuteRate() * rateFactor);
+        json.key("m15_rate").value(meter.getFifteenMinuteRate() * rateFactor);
+        json.key("mean_rate").value(meter.getMeanRate() * rateFactor);
+        json.key("units").value(rateUnit);
+        json.endObject();
+    }
+
+    private void printHistogram(Map.Entry<String, Histogram> e) throws JSONException {
+        json.key(e.getKey()).object();
+        json.key("count").value(e.getValue().getCount());
+
+        Snapshot snapshot = e.getValue().getSnapshot();
+        json.key("max").value(snapshot.getMax());
+        json.key("mean").value(snapshot.getMean());
+        json.key("min").value(snapshot.getMin());
+        json.key("p50").value(snapshot.getMedian());
+        json.key("p75").value(snapshot.get75thPercentile());
+        json.key("p95").value(snapshot.get95thPercentile());
+        json.key("p98").value(snapshot.get98thPercentile());
+        json.key("p99").value(snapshot.get99thPercentile());
+        json.key("p999").value(snapshot.get999thPercentile());
+        json.key("stddev").value(snapshot.getStdDev());
+
+        json.endObject();
+    }
+
+    private void printCounter(Map.Entry<String, Counter> e) throws JSONException {
+        json.key(e.getKey()).object();
+        json.key("count").value(e.getValue().getCount());
+        json.endObject();
+    }
+
+    private void printGauge(Map.Entry<String, Gauge> e) throws JSONException {
+        json.key(e.getKey()).object();
+        Object v = e.getValue().getValue();
+        json.key("value").value(jsonSafeValue(v));
+        json.endObject();
+    }
+
+    private static Object jsonSafeValue(Object v){
+        //Json does not allow NaN or infinite doubles. So take care of that
+        if (v instanceof Number){
+            if (v instanceof Double){
+                Double d = (Double) v;
+                if (d.isInfinite() || d.isNaN()){
+                    return d.toString();
+                }
+            }
+        }
+        return v;
+    }
+
+    private static String calculateRateUnit(TimeUnit unit) {
+        final String s = unit.toString().toLowerCase(Locale.US);
+        return s.substring(0, s.length() - 1);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/commons/metrics/internal/MetricWebConsolePlugin.java b/src/main/java/org/apache/sling/commons/metrics/internal/MetricWebConsolePlugin.java
index f5261c5..c33eb18 100644
--- a/src/main/java/org/apache/sling/commons/metrics/internal/MetricWebConsolePlugin.java
+++ b/src/main/java/org/apache/sling/commons/metrics/internal/MetricWebConsolePlugin.java
@@ -66,7 +66,7 @@
         @Property(name = "felix.webconsole.label", value = "slingmetrics"),
         @Property(name = "felix.webconsole.title", value = "Metrics"),
         @Property(name = "felix.webconsole.category", value = "Sling"),
-        @Property(name = InventoryPrinter.FORMAT, value = {"TEXT" }),
+        @Property(name = InventoryPrinter.FORMAT, value = {"TEXT" , "JSON"}),
         @Property(name = InventoryPrinter.NAME, value = "slingmetrics"),
         @Property(name = InventoryPrinter.TITLE, value = "Sling Metrics"),
         @Property(name = InventoryPrinter.WEBCONSOLE, boolValue = true)
@@ -114,6 +114,13 @@
                     .build();
             reporter.report();
             reporter.close();
+        } else if (format == Format.JSON) {
+            MetricRegistry registry = getConsolidatedRegistry();
+            JSONReporter reporter = JSONReporter.forRegistry(registry)
+                    .outputTo(new PrintStream(new WriterOutputStream(printWriter)))
+                    .build();
+            reporter.report();
+            reporter.close();
         }
     }
 
diff --git a/src/test/java/org/apache/sling/commons/metrics/internal/JSONReporterTest.java b/src/test/java/org/apache/sling/commons/metrics/internal/JSONReporterTest.java
new file mode 100644
index 0000000..1f13872
--- /dev/null
+++ b/src/test/java/org/apache/sling/commons/metrics/internal/JSONReporterTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sling.commons.metrics.internal;
+
+import java.io.PrintStream;
+import java.io.StringWriter;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.JvmAttributeGaugeSet;
+import com.codahale.metrics.MetricRegistry;
+import org.apache.commons.io.output.WriterOutputStream;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+
+public class JSONReporterTest {
+
+    @Test
+    public void jsonOutput() throws Exception {
+        MetricRegistry registry = new MetricRegistry();
+        registry.meter("test1").mark(5);
+        registry.timer("test2").time().close();
+        registry.histogram("test3").update(743);
+        registry.counter("test4").inc(9);
+        registry.registerAll(new JvmAttributeGaugeSet());
+
+        JSONObject json = getJSON(registry);
+
+        assertTrue(json.has("meters"));
+        assertTrue(json.has("guages"));
+        assertTrue(json.has("timers"));
+        assertTrue(json.has("counters"));
+        assertTrue(json.has("histograms"));
+        assertTrue(json.has("meters"));
+
+        assertTrue(json.getJSONObject("meters").has("test1"));
+        assertTrue(json.getJSONObject("timers").has("test2"));
+        assertTrue(json.getJSONObject("counters").has("test4"));
+        assertTrue(json.getJSONObject("histograms").has("test3"));
+    }
+
+    @Test
+    public void nan_value() throws Exception{
+        MetricRegistry registry = new MetricRegistry();
+
+        registry.register("test", new Gauge<Double>() {
+            @Override
+            public Double getValue() {
+                return Double.POSITIVE_INFINITY;
+            }
+        });
+
+
+        JSONObject json = getJSON(registry);
+        assertTrue(json.getJSONObject("guages").has("test"));
+    }
+
+    private static JSONObject getJSON(MetricRegistry registry) throws JSONException {
+        StringWriter sw = new StringWriter();
+        JSONReporter reporter = JSONReporter.forRegistry(registry)
+                .outputTo(new PrintStream(new WriterOutputStream(sw)))
+                .build();
+        reporter.report();
+        reporter.close();
+        return new JSONObject(sw.toString());
+    }
+
+}
\ No newline at end of file